diff --git a/composer.json b/composer.json
index 5c6e0f7b..8e2355c9 100644
--- a/composer.json
+++ b/composer.json
@@ -32,20 +32,21 @@
},
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
- "laminas/laminas-stdlib": "^2.7 || ^3.0"
+ "laminas/laminas-stdlib": "^2.7 || ^3.0",
+ "laminas/laminas-translator": "^1.1"
},
"require-dev": {
"laminas/laminas-coding-standard": "~3.0.1",
- "laminas/laminas-config": "^3.9.0",
- "laminas/laminas-http": "^2.19",
+ "laminas/laminas-config": "^3.10.0",
+ "laminas/laminas-http": "^2.20",
"laminas/laminas-i18n": "^2.29",
- "laminas/laminas-mvc": "^3.7",
+ "laminas/laminas-mvc": "^3.8",
"laminas/laminas-permissions-acl": "^2.16",
"laminas/laminas-router": "^3.14.0",
- "laminas/laminas-servicemanager": "^3.22.1",
+ "laminas/laminas-servicemanager": "^3.23.0",
"laminas/laminas-uri": "^2.12.0",
- "laminas/laminas-view": "^2.35",
- "phpunit/phpunit": "^9.6.21",
+ "laminas/laminas-view": "^2.36",
+ "phpunit/phpunit": "^10.5.38",
"psalm/plugin-phpunit": "^0.19.0",
"vimeo/psalm": "^5.26.1"
},
diff --git a/composer.lock b/composer.lock
index bd67acaf..ecfa3a8a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "6ff3519b0fe044983c831e239499c375",
+ "content-hash": "d9b5d4495e6ecacf7b4fbf83d810d995",
"packages": [
{
"name": "laminas/laminas-stdlib",
@@ -64,6 +64,59 @@
}
],
"time": "2024-10-29T13:46:07+00:00"
+ },
+ {
+ "name": "laminas/laminas-translator",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-translator.git",
+ "reference": "12897e710e21413c1f93fc38fe9dead6b51c5218"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-translator/zipball/12897e710e21413c1f93fc38fe9dead6b51c5218",
+ "reference": "12897e710e21413c1f93fc38fe9dead6b51c5218",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "~3.0.0",
+ "vimeo/psalm": "^5.24.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Laminas\\Translator\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Interfaces for the Translator component of laminas-i18n",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "i18n",
+ "laminas"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-i18n/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-translator/issues",
+ "rss": "https://github.com/laminas/laminas-translator/releases.atom",
+ "source": "https://github.com/laminas/laminas-translator"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2024-10-21T15:33:01+00:00"
}
],
"packages-dev": [
@@ -737,76 +790,6 @@
},
"time": "2024-01-30T19:34:25+00:00"
},
- {
- "name": "doctrine/instantiator",
- "version": "2.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/instantiator.git",
- "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
- "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
- "shasum": ""
- },
- "require": {
- "php": "^8.1"
- },
- "require-dev": {
- "doctrine/coding-standard": "^11",
- "ext-pdo": "*",
- "ext-phar": "*",
- "phpbench/phpbench": "^1.2",
- "phpstan/phpstan": "^1.9.4",
- "phpstan/phpstan-phpunit": "^1.3",
- "phpunit/phpunit": "^9.5.27",
- "vimeo/psalm": "^5.4"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Marco Pivetta",
- "email": "ocramius@gmail.com",
- "homepage": "https://ocramius.github.io/"
- }
- ],
- "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
- "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
- "keywords": [
- "constructor",
- "instantiate"
- ],
- "support": {
- "issues": "https://github.com/doctrine/instantiator/issues",
- "source": "https://github.com/doctrine/instantiator/tree/2.0.0"
- },
- "funding": [
- {
- "url": "https://www.doctrine-project.org/sponsorship.html",
- "type": "custom"
- },
- {
- "url": "https://www.patreon.com/phpdoctrine",
- "type": "patreon"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
- "type": "tidelift"
- }
- ],
- "time": "2022-12-30T00:23:10+00:00"
- },
{
"name": "felixfbecker/advanced-json-rpc",
"version": "v3.2.1",
@@ -1088,6 +1071,7 @@
"type": "community_bridge"
}
],
+ "abandoned": true,
"time": "2024-11-17T22:10:53+00:00"
},
{
@@ -1154,33 +1138,33 @@
},
{
"name": "laminas/laminas-eventmanager",
- "version": "3.13.1",
+ "version": "3.14.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-eventmanager.git",
- "reference": "933d1b5cf03fa4cf3016cebfd0555fa2ba3f2024"
+ "reference": "1837cafaaaee74437f6d8ec9ff7da03e6f81d809"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/933d1b5cf03fa4cf3016cebfd0555fa2ba3f2024",
- "reference": "933d1b5cf03fa4cf3016cebfd0555fa2ba3f2024",
+ "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/1837cafaaaee74437f6d8ec9ff7da03e6f81d809",
+ "reference": "1837cafaaaee74437f6d8ec9ff7da03e6f81d809",
"shasum": ""
},
"require": {
- "php": "~8.1.0 || ~8.2.0 || ~8.3.0"
+ "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"conflict": {
"container-interop/container-interop": "<1.2",
"zendframework/zend-eventmanager": "*"
},
"require-dev": {
- "laminas/laminas-coding-standard": "~2.5.0",
- "laminas/laminas-stdlib": "^3.18",
- "phpbench/phpbench": "^1.2.15",
- "phpunit/phpunit": "^10.5.5",
- "psalm/plugin-phpunit": "^0.18.4",
+ "laminas/laminas-coding-standard": "~3.0.0",
+ "laminas/laminas-stdlib": "^3.20",
+ "phpbench/phpbench": "^1.3.1",
+ "phpunit/phpunit": "^10.5.38",
+ "psalm/plugin-phpunit": "^0.19.0",
"psr/container": "^1.1.2 || ^2.0.2",
- "vimeo/psalm": "^5.18"
+ "vimeo/psalm": "^5.26.1"
},
"suggest": {
"laminas/laminas-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature",
@@ -1218,7 +1202,7 @@
"type": "community_bridge"
}
],
- "time": "2024-06-24T14:01:06+00:00"
+ "time": "2024-11-21T11:31:22+00:00"
},
{
"name": "laminas/laminas-http",
@@ -1430,6 +1414,7 @@
"type": "community_bridge"
}
],
+ "abandoned": true,
"time": "2024-10-25T09:02:25+00:00"
},
{
@@ -1486,6 +1471,7 @@
"type": "community_bridge"
}
],
+ "abandoned": true,
"time": "2024-10-16T09:06:57+00:00"
},
{
@@ -1563,28 +1549,28 @@
},
{
"name": "laminas/laminas-mvc",
- "version": "3.7.0",
+ "version": "3.8.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-mvc.git",
- "reference": "3f65447addf487189000e54dc1525cd952951da4"
+ "reference": "53ba28b7222d3a3b49747a26babef43d1b17fb6f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laminas/laminas-mvc/zipball/3f65447addf487189000e54dc1525cd952951da4",
- "reference": "3f65447addf487189000e54dc1525cd952951da4",
+ "url": "https://api.github.com/repos/laminas/laminas-mvc/zipball/53ba28b7222d3a3b49747a26babef43d1b17fb6f",
+ "reference": "53ba28b7222d3a3b49747a26babef43d1b17fb6f",
"shasum": ""
},
"require": {
"container-interop/container-interop": "^1.2",
"laminas/laminas-eventmanager": "^3.4",
"laminas/laminas-http": "^2.15",
- "laminas/laminas-modulemanager": "^2.8",
+ "laminas/laminas-modulemanager": "^2.16",
"laminas/laminas-router": "^3.11.1",
"laminas/laminas-servicemanager": "^3.20.0",
- "laminas/laminas-stdlib": "^3.6",
- "laminas/laminas-view": "^2.14",
- "php": "~8.1.0 || ~8.2.0 || ~8.3.0"
+ "laminas/laminas-stdlib": "^3.19",
+ "laminas/laminas-view": "^2.18.0",
+ "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"conflict": {
"zendframework/zend-mvc": "*"
@@ -1592,9 +1578,7 @@
"require-dev": {
"laminas/laminas-coding-standard": "^2.5.0",
"laminas/laminas-json": "^3.6",
- "phpspec/prophecy": "^1.17.0",
- "phpspec/prophecy-phpunit": "^2.0.2",
- "phpunit/phpunit": "^9.6.13",
+ "phpunit/phpunit": "^10.5.38",
"webmozart/assert": "^1.11"
},
"suggest": {
@@ -1640,7 +1624,7 @@
"type": "community_bridge"
}
],
- "time": "2023-11-14T09:44:53+00:00"
+ "time": "2024-11-18T00:14:29+00:00"
},
{
"name": "laminas/laminas-permissions-acl",
@@ -1867,59 +1851,6 @@
],
"time": "2024-10-28T21:32:16+00:00"
},
- {
- "name": "laminas/laminas-translator",
- "version": "1.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/laminas/laminas-translator.git",
- "reference": "12897e710e21413c1f93fc38fe9dead6b51c5218"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/laminas/laminas-translator/zipball/12897e710e21413c1f93fc38fe9dead6b51c5218",
- "reference": "12897e710e21413c1f93fc38fe9dead6b51c5218",
- "shasum": ""
- },
- "require": {
- "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
- },
- "require-dev": {
- "laminas/laminas-coding-standard": "~3.0.0",
- "vimeo/psalm": "^5.24.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Laminas\\Translator\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "description": "Interfaces for the Translator component of laminas-i18n",
- "homepage": "https://laminas.dev",
- "keywords": [
- "i18n",
- "laminas"
- ],
- "support": {
- "chat": "https://laminas.dev/chat",
- "docs": "https://docs.laminas.dev/laminas-i18n/",
- "forum": "https://discourse.laminas.dev",
- "issues": "https://github.com/laminas/laminas-translator/issues",
- "rss": "https://github.com/laminas/laminas-translator/releases.atom",
- "source": "https://github.com/laminas/laminas-translator"
- },
- "funding": [
- {
- "url": "https://funding.communitybridge.org/projects/laminas-project",
- "type": "community_bridge"
- }
- ],
- "time": "2024-10-21T15:33:01+00:00"
- },
{
"name": "laminas/laminas-uri",
"version": "2.12.0",
@@ -2064,16 +1995,16 @@
},
{
"name": "laminas/laminas-view",
- "version": "2.35.0",
+ "version": "2.36.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-view.git",
- "reference": "f597148345dd406fb9d04d391a19c0c33bf71605"
+ "reference": "ddc9207725cb50508ea48fcf1210dc8480264196"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laminas/laminas-view/zipball/f597148345dd406fb9d04d391a19c0c33bf71605",
- "reference": "f597148345dd406fb9d04d391a19c0c33bf71605",
+ "url": "https://api.github.com/repos/laminas/laminas-view/zipball/ddc9207725cb50508ea48fcf1210dc8480264196",
+ "reference": "ddc9207725cb50508ea48fcf1210dc8480264196",
"shasum": ""
},
"require": {
@@ -2085,7 +2016,7 @@
"laminas/laminas-json": "^3.3",
"laminas/laminas-servicemanager": "^3.21.0",
"laminas/laminas-stdlib": "^3.10.1",
- "php": "~8.1.0 || ~8.2.0 || ~8.3.0",
+ "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"psr/container": "^1 || ^2"
},
"conflict": {
@@ -2095,24 +2026,24 @@
"zendframework/zend-view": "*"
},
"require-dev": {
- "laminas/laminas-authentication": "^2.16",
+ "laminas/laminas-authentication": "^2.18",
"laminas/laminas-coding-standard": "~2.5.0",
- "laminas/laminas-feed": "^2.22",
- "laminas/laminas-filter": "^2.34",
- "laminas/laminas-http": "^2.19",
- "laminas/laminas-i18n": "^2.26.0",
- "laminas/laminas-modulemanager": "^2.15",
- "laminas/laminas-mvc": "^3.7.0",
- "laminas/laminas-mvc-i18n": "^1.8",
+ "laminas/laminas-feed": "^2.23",
+ "laminas/laminas-filter": "^2.39",
+ "laminas/laminas-http": "^2.20",
+ "laminas/laminas-i18n": "^2.29.0",
+ "laminas/laminas-modulemanager": "^2.17",
+ "laminas/laminas-mvc": "^3.8.0",
+ "laminas/laminas-mvc-i18n": "^1.9",
"laminas/laminas-mvc-plugin-flashmessenger": "^1.10.1",
- "laminas/laminas-navigation": "^2.19.1",
- "laminas/laminas-paginator": "^2.18.1",
+ "laminas/laminas-navigation": "^2.20.0",
+ "laminas/laminas-paginator": "^2.19.0",
"laminas/laminas-permissions-acl": "^2.16",
- "laminas/laminas-router": "^3.13.0",
- "laminas/laminas-uri": "^2.11",
- "phpunit/phpunit": "^10.5.13",
+ "laminas/laminas-router": "^3.14.0",
+ "laminas/laminas-uri": "^2.12",
+ "phpunit/phpunit": "^10.5.38",
"psalm/plugin-phpunit": "^0.19.0",
- "vimeo/psalm": "^5.23.1"
+ "vimeo/psalm": "^5.26.1"
},
"suggest": {
"laminas/laminas-authentication": "Laminas\\Authentication component",
@@ -2160,7 +2091,7 @@
"type": "community_bridge"
}
],
- "time": "2024-06-04T06:44:31+00:00"
+ "time": "2024-11-21T17:42:20+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -2671,16 +2602,16 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "9.2.32",
+ "version": "10.1.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
- "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77",
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77",
"shasum": ""
},
"require": {
@@ -2688,18 +2619,18 @@
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.19.1 || ^5.1.0",
- "php": ">=7.3",
- "phpunit/php-file-iterator": "^3.0.6",
- "phpunit/php-text-template": "^2.0.4",
- "sebastian/code-unit-reverse-lookup": "^2.0.3",
- "sebastian/complexity": "^2.0.3",
- "sebastian/environment": "^5.1.5",
- "sebastian/lines-of-code": "^1.0.4",
- "sebastian/version": "^3.0.2",
+ "php": ">=8.1",
+ "phpunit/php-file-iterator": "^4.1.0",
+ "phpunit/php-text-template": "^3.0.1",
+ "sebastian/code-unit-reverse-lookup": "^3.0.0",
+ "sebastian/complexity": "^3.2.0",
+ "sebastian/environment": "^6.1.0",
+ "sebastian/lines-of-code": "^2.0.2",
+ "sebastian/version": "^4.0.1",
"theseer/tokenizer": "^1.2.3"
},
"require-dev": {
- "phpunit/phpunit": "^9.6"
+ "phpunit/phpunit": "^10.1"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -2708,7 +2639,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "9.2.x-dev"
+ "dev-main": "10.1.x-dev"
}
},
"autoload": {
@@ -2737,7 +2668,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16"
},
"funding": [
{
@@ -2745,32 +2676,32 @@
"type": "github"
}
],
- "time": "2024-08-22T04:23:01+00:00"
+ "time": "2024-08-22T04:31:57+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "3.0.6",
+ "version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
- "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c",
+ "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -2797,7 +2728,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
- "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0"
},
"funding": [
{
@@ -2805,28 +2737,28 @@
"type": "github"
}
],
- "time": "2021-12-02T12:48:52+00:00"
+ "time": "2023-08-31T06:24:48+00:00"
},
{
"name": "phpunit/php-invoker",
- "version": "3.1.1",
+ "version": "4.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-invoker.git",
- "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
- "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
+ "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
"ext-pcntl": "*",
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"suggest": {
"ext-pcntl": "*"
@@ -2834,7 +2766,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -2860,7 +2792,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-invoker/issues",
- "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0"
},
"funding": [
{
@@ -2868,32 +2800,32 @@
"type": "github"
}
],
- "time": "2020-09-28T05:58:55+00:00"
+ "time": "2023-02-03T06:56:09+00:00"
},
{
"name": "phpunit/php-text-template",
- "version": "2.0.4",
+ "version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
- "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
- "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748",
+ "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -2919,7 +2851,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-text-template/issues",
- "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1"
},
"funding": [
{
@@ -2927,32 +2860,32 @@
"type": "github"
}
],
- "time": "2020-10-26T05:33:50+00:00"
+ "time": "2023-08-31T14:07:24+00:00"
},
{
"name": "phpunit/php-timer",
- "version": "5.0.3",
+ "version": "6.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
- "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d",
+ "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-main": "6.0-dev"
}
},
"autoload": {
@@ -2978,7 +2911,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-timer/issues",
- "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0"
},
"funding": [
{
@@ -2986,24 +2919,23 @@
"type": "github"
}
],
- "time": "2020-10-26T13:16:10+00:00"
+ "time": "2023-02-03T06:57:52+00:00"
},
{
"name": "phpunit/phpunit",
- "version": "9.6.21",
+ "version": "10.5.38",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa"
+ "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
- "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132",
+ "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.5.0 || ^2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
@@ -3013,27 +2945,26 @@
"myclabs/deep-copy": "^1.12.0",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
- "php": ">=7.3",
- "phpunit/php-code-coverage": "^9.2.32",
- "phpunit/php-file-iterator": "^3.0.6",
- "phpunit/php-invoker": "^3.1.1",
- "phpunit/php-text-template": "^2.0.4",
- "phpunit/php-timer": "^5.0.3",
- "sebastian/cli-parser": "^1.0.2",
- "sebastian/code-unit": "^1.0.8",
- "sebastian/comparator": "^4.0.8",
- "sebastian/diff": "^4.0.6",
- "sebastian/environment": "^5.1.5",
- "sebastian/exporter": "^4.0.6",
- "sebastian/global-state": "^5.0.7",
- "sebastian/object-enumerator": "^4.0.4",
- "sebastian/resource-operations": "^3.0.4",
- "sebastian/type": "^3.2.1",
- "sebastian/version": "^3.0.2"
+ "php": ">=8.1",
+ "phpunit/php-code-coverage": "^10.1.16",
+ "phpunit/php-file-iterator": "^4.1.0",
+ "phpunit/php-invoker": "^4.0.0",
+ "phpunit/php-text-template": "^3.0.1",
+ "phpunit/php-timer": "^6.0.0",
+ "sebastian/cli-parser": "^2.0.1",
+ "sebastian/code-unit": "^2.0.0",
+ "sebastian/comparator": "^5.0.3",
+ "sebastian/diff": "^5.1.1",
+ "sebastian/environment": "^6.1.0",
+ "sebastian/exporter": "^5.1.2",
+ "sebastian/global-state": "^6.0.2",
+ "sebastian/object-enumerator": "^5.0.0",
+ "sebastian/recursion-context": "^5.0.0",
+ "sebastian/type": "^4.0.0",
+ "sebastian/version": "^4.0.1"
},
"suggest": {
- "ext-soap": "To be able to generate mocks based on WSDL files",
- "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ "ext-soap": "To be able to generate mocks based on WSDL files"
},
"bin": [
"phpunit"
@@ -3041,7 +2972,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "9.6-dev"
+ "dev-main": "10.5-dev"
}
},
"autoload": {
@@ -3073,7 +3004,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.38"
},
"funding": [
{
@@ -3089,7 +3020,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-19T10:50:18+00:00"
+ "time": "2024-10-28T13:06:21+00:00"
},
{
"name": "psalm/plugin-phpunit",
@@ -3304,28 +3235,28 @@
},
{
"name": "sebastian/cli-parser",
- "version": "1.0.2",
+ "version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/cli-parser.git",
- "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+ "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
- "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084",
+ "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-main": "2.0-dev"
}
},
"autoload": {
@@ -3348,7 +3279,8 @@
"homepage": "https://github.com/sebastianbergmann/cli-parser",
"support": {
"issues": "https://github.com/sebastianbergmann/cli-parser/issues",
- "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1"
},
"funding": [
{
@@ -3356,32 +3288,32 @@
"type": "github"
}
],
- "time": "2024-03-02T06:27:43+00:00"
+ "time": "2024-03-02T07:12:49+00:00"
},
{
"name": "sebastian/code-unit",
- "version": "1.0.8",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit.git",
- "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ "reference": "a81fee9eef0b7a76af11d121767abc44c104e503"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
- "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503",
+ "reference": "a81fee9eef0b7a76af11d121767abc44c104e503",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-main": "2.0-dev"
}
},
"autoload": {
@@ -3404,7 +3336,7 @@
"homepage": "https://github.com/sebastianbergmann/code-unit",
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit/issues",
- "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0"
},
"funding": [
{
@@ -3412,32 +3344,32 @@
"type": "github"
}
],
- "time": "2020-10-26T13:08:54+00:00"
+ "time": "2023-02-03T06:58:43+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
- "version": "2.0.3",
+ "version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
- "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
+ "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -3459,7 +3391,7 @@
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
- "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0"
},
"funding": [
{
@@ -3467,34 +3399,36 @@
"type": "github"
}
],
- "time": "2020-09-28T05:30:19+00:00"
+ "time": "2023-02-03T06:59:15+00:00"
},
{
"name": "sebastian/comparator",
- "version": "4.0.8",
+ "version": "5.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+ "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
- "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
+ "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
"shasum": ""
},
"require": {
- "php": ">=7.3",
- "sebastian/diff": "^4.0",
- "sebastian/exporter": "^4.0"
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.1",
+ "sebastian/diff": "^5.0",
+ "sebastian/exporter": "^5.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "5.0-dev"
}
},
"autoload": {
@@ -3533,7 +3467,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
- "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3"
},
"funding": [
{
@@ -3541,33 +3476,33 @@
"type": "github"
}
],
- "time": "2022-09-14T12:41:17+00:00"
+ "time": "2024-10-18T14:56:07+00:00"
},
{
"name": "sebastian/complexity",
- "version": "2.0.3",
+ "version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/complexity.git",
- "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ "reference": "68ff824baeae169ec9f2137158ee529584553799"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
- "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799",
+ "reference": "68ff824baeae169ec9f2137158ee529584553799",
"shasum": ""
},
"require": {
"nikic/php-parser": "^4.18 || ^5.0",
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "3.2-dev"
}
},
"autoload": {
@@ -3590,7 +3525,8 @@
"homepage": "https://github.com/sebastianbergmann/complexity",
"support": {
"issues": "https://github.com/sebastianbergmann/complexity/issues",
- "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0"
},
"funding": [
{
@@ -3598,33 +3534,33 @@
"type": "github"
}
],
- "time": "2023-12-22T06:19:30+00:00"
+ "time": "2023-12-21T08:37:17+00:00"
},
{
"name": "sebastian/diff",
- "version": "4.0.6",
+ "version": "5.1.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+ "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
- "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e",
+ "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3",
- "symfony/process": "^4.2 || ^5"
+ "phpunit/phpunit": "^10.0",
+ "symfony/process": "^6.4"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "5.1-dev"
}
},
"autoload": {
@@ -3656,7 +3592,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
- "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1"
},
"funding": [
{
@@ -3664,27 +3601,27 @@
"type": "github"
}
],
- "time": "2024-03-02T06:30:58+00:00"
+ "time": "2024-03-02T07:15:17+00:00"
},
{
"name": "sebastian/environment",
- "version": "5.1.5",
+ "version": "6.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ "reference": "8074dbcd93529b357029f5cc5058fd3e43666984"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
- "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984",
+ "reference": "8074dbcd93529b357029f5cc5058fd3e43666984",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"suggest": {
"ext-posix": "*"
@@ -3692,7 +3629,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.1-dev"
+ "dev-main": "6.1-dev"
}
},
"autoload": {
@@ -3711,7 +3648,7 @@
}
],
"description": "Provides functionality to handle HHVM/PHP environments",
- "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "homepage": "https://github.com/sebastianbergmann/environment",
"keywords": [
"Xdebug",
"environment",
@@ -3719,7 +3656,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
- "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ "security": "https://github.com/sebastianbergmann/environment/security/policy",
+ "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0"
},
"funding": [
{
@@ -3727,34 +3665,34 @@
"type": "github"
}
],
- "time": "2023-02-03T06:03:51+00:00"
+ "time": "2024-03-23T08:47:14+00:00"
},
{
"name": "sebastian/exporter",
- "version": "4.0.6",
+ "version": "5.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
+ "reference": "955288482d97c19a372d3f31006ab3f37da47adf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
- "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf",
+ "reference": "955288482d97c19a372d3f31006ab3f37da47adf",
"shasum": ""
},
"require": {
- "php": ">=7.3",
- "sebastian/recursion-context": "^4.0"
+ "ext-mbstring": "*",
+ "php": ">=8.1",
+ "sebastian/recursion-context": "^5.0"
},
"require-dev": {
- "ext-mbstring": "*",
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "5.1-dev"
}
},
"autoload": {
@@ -3796,7 +3734,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
- "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2"
},
"funding": [
{
@@ -3804,38 +3743,35 @@
"type": "github"
}
],
- "time": "2024-03-02T06:33:00+00:00"
+ "time": "2024-03-02T07:17:12+00:00"
},
{
"name": "sebastian/global-state",
- "version": "5.0.7",
+ "version": "6.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
+ "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
- "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
+ "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
"shasum": ""
},
"require": {
- "php": ">=7.3",
- "sebastian/object-reflector": "^2.0",
- "sebastian/recursion-context": "^4.0"
+ "php": ">=8.1",
+ "sebastian/object-reflector": "^3.0",
+ "sebastian/recursion-context": "^5.0"
},
"require-dev": {
"ext-dom": "*",
- "phpunit/phpunit": "^9.3"
- },
- "suggest": {
- "ext-uopz": "*"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-main": "6.0-dev"
}
},
"autoload": {
@@ -3854,13 +3790,14 @@
}
],
"description": "Snapshotting of global state",
- "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state",
"keywords": [
"global state"
],
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
- "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2"
},
"funding": [
{
@@ -3868,33 +3805,33 @@
"type": "github"
}
],
- "time": "2024-03-02T06:35:11+00:00"
+ "time": "2024-03-02T07:19:19+00:00"
},
{
"name": "sebastian/lines-of-code",
- "version": "1.0.4",
+ "version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/lines-of-code.git",
- "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
- "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0",
+ "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0",
"shasum": ""
},
"require": {
"nikic/php-parser": "^4.18 || ^5.0",
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-main": "2.0-dev"
}
},
"autoload": {
@@ -3917,7 +3854,8 @@
"homepage": "https://github.com/sebastianbergmann/lines-of-code",
"support": {
"issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
- "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2"
},
"funding": [
{
@@ -3925,34 +3863,34 @@
"type": "github"
}
],
- "time": "2023-12-22T06:20:34+00:00"
+ "time": "2023-12-21T08:38:20+00:00"
},
{
"name": "sebastian/object-enumerator",
- "version": "4.0.4",
+ "version": "5.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
- "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
- "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906",
+ "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906",
"shasum": ""
},
"require": {
- "php": ">=7.3",
- "sebastian/object-reflector": "^2.0",
- "sebastian/recursion-context": "^4.0"
+ "php": ">=8.1",
+ "sebastian/object-reflector": "^3.0",
+ "sebastian/recursion-context": "^5.0"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "5.0-dev"
}
},
"autoload": {
@@ -3974,7 +3912,7 @@
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
- "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0"
},
"funding": [
{
@@ -3982,32 +3920,32 @@
"type": "github"
}
],
- "time": "2020-10-26T13:12:34+00:00"
+ "time": "2023-02-03T07:08:32+00:00"
},
{
"name": "sebastian/object-reflector",
- "version": "2.0.4",
+ "version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-reflector.git",
- "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ "reference": "24ed13d98130f0e7122df55d06c5c4942a577957"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
- "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957",
+ "reference": "24ed13d98130f0e7122df55d06c5c4942a577957",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -4029,7 +3967,7 @@
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-reflector/issues",
- "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0"
},
"funding": [
{
@@ -4037,32 +3975,32 @@
"type": "github"
}
],
- "time": "2020-10-26T13:14:26+00:00"
+ "time": "2023-02-03T07:06:18+00:00"
},
{
"name": "sebastian/recursion-context",
- "version": "4.0.5",
+ "version": "5.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
+ "reference": "05909fb5bc7df4c52992396d0116aed689f93712"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
- "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712",
+ "reference": "05909fb5bc7df4c52992396d0116aed689f93712",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.3"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-main": "5.0-dev"
}
},
"autoload": {
@@ -4092,7 +4030,7 @@
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0"
},
"funding": [
{
@@ -4100,86 +4038,32 @@
"type": "github"
}
],
- "time": "2023-02-03T06:07:39+00:00"
- },
- {
- "name": "sebastian/resource-operations",
- "version": "3.0.4",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
- "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
- "shasum": ""
- },
- "require": {
- "php": ">=7.3"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "3.0-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Provides a list of PHP built-in functions that operate on resources",
- "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "support": {
- "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
- },
- "funding": [
- {
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
- }
- ],
- "time": "2024-03-14T16:00:52+00:00"
+ "time": "2023-02-03T07:05:40+00:00"
},
{
"name": "sebastian/type",
- "version": "3.2.1",
+ "version": "4.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
- "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ "reference": "462699a16464c3944eefc02ebdd77882bd3925bf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
- "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf",
+ "reference": "462699a16464c3944eefc02ebdd77882bd3925bf",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^9.5"
+ "phpunit/phpunit": "^10.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.2-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -4202,7 +4086,7 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
- "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ "source": "https://github.com/sebastianbergmann/type/tree/4.0.0"
},
"funding": [
{
@@ -4210,29 +4094,29 @@
"type": "github"
}
],
- "time": "2023-02-03T06:13:03+00:00"
+ "time": "2023-02-03T07:10:45+00:00"
},
{
"name": "sebastian/version",
- "version": "3.0.2",
+ "version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/version.git",
- "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
- "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17",
+ "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17",
"shasum": ""
},
"require": {
- "php": ">=7.3"
+ "php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -4255,7 +4139,7 @@
"homepage": "https://github.com/sebastianbergmann/version",
"support": {
"issues": "https://github.com/sebastianbergmann/version/issues",
- "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ "source": "https://github.com/sebastianbergmann/version/tree/4.0.1"
},
"funding": [
{
@@ -4263,7 +4147,7 @@
"type": "github"
}
],
- "time": "2020-09-28T06:39:44+00:00"
+ "time": "2023-02-07T11:34:05+00:00"
},
{
"name": "slevomat/coding-standard",
@@ -5527,13 +5411,13 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": {},
+ "stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
- "platform-dev": {},
+ "platform-dev": [],
"platform-overrides": {
"php": "8.1.99"
},
diff --git a/docs/book/cookbook/mvc-sitemap.md b/docs/book/cookbook/mvc-sitemap.md
index 5b5cd15b..e146fed8 100644
--- a/docs/book/cookbook/mvc-sitemap.md
+++ b/docs/book/cookbook/mvc-sitemap.md
@@ -40,7 +40,7 @@ With the [custom response type `Laminas\Diactoros\Response\XmlResponse`](https:/
namespace Application\Handler;
use Laminas\Diactoros\Response\XmlResponse;
-use Laminas\View\Helper\Navigation\Sitemap;
+use Laminas\Navigation\View\Helper\Sitemap;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -70,8 +70,8 @@ Fetch the [Navigation Proxy helper](../helpers/navigation.md) from the [view hel
```php
namespace Application\Handler;
-use Laminas\View\Helper\Navigation as NavigationProxyHelper;
-use Laminas\View\Helper\Navigation\Sitemap;
+use Laminas\Navigation\View\Helper as NavigationProxyHelper;
+use Laminas\Navigation\View\Helper\Sitemap;
use Laminas\View\HelperPluginManager;
use Psr\Container\ContainerInterface;
@@ -184,7 +184,7 @@ namespace Application\Controller;
use Laminas\Http\Response;
use Laminas\Mvc\Controller\AbstractActionController;
-use Laminas\View\Helper\Navigation\Sitemap;
+use Laminas\Navigation\View\Helper\Sitemap;
class IndexController extends AbstractActionController
{
diff --git a/docs/book/helpers/links.md b/docs/book/helpers/links.md
index 0e9fe96d..951330fd 100644
--- a/docs/book/helpers/links.md
+++ b/docs/book/helpers/links.md
@@ -193,7 +193,7 @@ This example shows how to specify which relations to find and render.
Render only start, next, and prev:
```php
-use Laminas\View\Helper\Navigation\Links;
+use Laminas\Navigation\View\Helper\Links;
$links = $this->navigation()->links();
$links->setRenderFlag(Links::RENDER_START | Links::RENDER_NEXT | Links::RENDER_PREV);
diff --git a/phpcs.xml b/phpcs.xml
index cd255f3f..b372225a 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -9,7 +9,7 @@
-
+
src
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 7c577470..ef320bd0 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,34 +1,37 @@
-
-
-
- ./test/
-
-
-
-
-
- disable
-
-
-
-
-
- ./src
-
-
-
-
-
-
-
-
-
-
+
+
+
+ ./test/
+
+
+
+
+ disable
+
+
+
+
+
+
+
+ ./src
+
+
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index f5afcbc5..d4f97a39 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -39,6 +39,12 @@
+
+
+
+
+
+
@@ -48,6 +54,11 @@
+
+
+
+
+
@@ -196,6 +207,15 @@
+
+
+
+
+
+
+
+
+
@@ -409,6 +429,492 @@
config !== null]]>
+
+
+
+
+ container]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getTextDomain()]]>
+ getTextDomain()]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ last()]]>
+
+
+
+
+
+
+
+
+ getTitle()]]>
+
+
+
+
+
+
+
+
+
+
+
+
+ container]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ renderPartialModel($params, $container, $partial)]]>
+ renderPartialModel([], $container, $partial)]]>
+
+
+
+
+
+
+
+ getLabel()]]>
+ getTextDomain()]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $meth()]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ root]]>
+
+
+
+ findFromProperty($page, $rel, $type)]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ renderPartialModel($params, $container, $partial)]]>
+ renderPartialModel([], $container, $partial)]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getTextDomain()]]>
+ getTextDomain()]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getTitle()]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getClass()]]>
+
+ getClass()]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ serverUrl]]>
+
+
+
+
+
+
+
+
+
+
+
+
+ serverUrl]]>
+ serverUrl]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ serverUrl)]]>
+
+
+ url($page)]]>
+
+
+
+
+
+
+
@@ -468,6 +974,9 @@
getValue($container) ?: $container]]>
getValue($container) ?: $container]]>
+
+
+
getServiceLocator()]]>
@@ -522,16 +1031,6 @@
-
- fail('An invalid argument was given to the constructor, '
- . 'but a Laminas\Navigation\Exception\InvalidArgumentException was '
- . 'not thrown');
- } catch (Navigation\Exception\ExceptionInterface $e) {
- $this->assertStringContainsString('Invalid argument: $pages', $e->getMessage());
- }]]>
-
@@ -647,6 +1146,9 @@
+
+
+
@@ -658,22 +1160,6 @@
-
- routeMatch]]>
- routeMatch]]>
- routeMatch]]>
- router]]>
- router]]>
- router]]>
- router]]>
- router]]>
-
-
-
-
-
-
-
@@ -692,25 +1178,6 @@
getRouteMatch()]]>
getRouteMatch()]]>
-
- route]]>
- routeMatch]]>
- router]]>
-
-
- routeMatch]]>
- routeMatch]]>
- routeMatch]]>
- routeMatch]]>
- router]]>
- router]]>
- router]]>
- router]]>
- router]]>
- router]]>
- router]]>
- router]]>
-
@@ -788,22 +1255,15 @@
+
+
+
-
- factory]]>
- factory]]>
-
-
- factory]]>
-
-
- factory]]>
-
@@ -814,9 +1274,6 @@
-
-
-
@@ -854,22 +1311,32 @@
-
- mvcEvent]]>
- request]]>
- router]]>
-
+
+
+
+
+
+
+
+ translations]]>
+
+
+
+
+
+
+
@@ -886,13 +1353,331 @@
+
+
+ serviceManager->get('Navigation')]]>
+
+
+
+
+ getDependencyConfig())]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ helper->renderMenu()]]>
+ helper->renderMenu(null, $renderOptions)]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ errorHandlerMessage = $message;
+ }]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ errorHandlerMessage]]>
+
+
+ 'unknownresource',
+ 'privilege' => 'someprivilege',
+ ], false)]]>
+
+
+
+
+ [
+ 'default' => [],
+ ],
+ ])]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
diff --git a/psalm.xml b/psalm.xml
index 22533683..06074c64 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -5,6 +5,9 @@
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="psalm-baseline.xml"
+ findUnusedBaselineEntry="true"
+ findUnusedPsalmSuppress="true"
+ findUnusedCode="true"
>
diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php
index 08ae01f0..66a8a393 100644
--- a/src/Exception/ExceptionInterface.php
+++ b/src/Exception/ExceptionInterface.php
@@ -4,9 +4,11 @@
namespace Laminas\Navigation\Exception;
+use Throwable;
+
/**
* Navigation exception
*/
-interface ExceptionInterface
+interface ExceptionInterface extends Throwable
{
}
diff --git a/src/View/Helper/AbstractHelper.php b/src/View/Helper/AbstractHelper.php
new file mode 100644
index 00000000..b347927c
--- /dev/null
+++ b/src/View/Helper/AbstractHelper.php
@@ -0,0 +1,901 @@
+getContainer(), $method],
+ $arguments
+ );
+ }
+
+ /**
+ * Magic overload: Proxy to {@link render()}.
+ *
+ * This method will trigger an E_USER_ERROR if rendering the helper causes
+ * an exception to be thrown.
+ *
+ * Implements {@link HelperInterface::__toString()}.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (\Exception $e) {
+ $msg = $e::class . ': ' . $e->getMessage();
+ trigger_error($msg, E_USER_ERROR);
+ return '';
+ }
+ }
+
+ /**
+ * Finds the deepest active page in the given container
+ *
+ * @param AbstractContainer $container container to search
+ * @param int|null $minDepth [optional] minimum depth
+ * required for page to be
+ * valid. Default is to use
+ * {@link getMinDepth()}. A
+ * null value means no minimum
+ * depth required.
+ * @param int|null $maxDepth [optional] maximum depth
+ * a page can have to be
+ * valid. Default is to use
+ * {@link getMaxDepth()}. A
+ * null value means no maximum
+ * depth required.
+ * @return array an associative array with
+ * the values 'depth' and
+ * 'page', or an empty array
+ * if not found
+ */
+ public function findActive($container, $minDepth = null, $maxDepth = -1)
+ {
+ $this->parseContainer($container);
+ if (! is_int($minDepth)) {
+ $minDepth = $this->getMinDepth();
+ }
+ if ((! is_int($maxDepth) || $maxDepth < 0) && null !== $maxDepth) {
+ $maxDepth = $this->getMaxDepth();
+ }
+
+ $found = null;
+ $foundDepth = -1;
+ $iterator = new RecursiveIteratorIterator(
+ $container,
+ RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ /** @var Navigation\Page\AbstractPage $page */
+ foreach ($iterator as $page) {
+ $currDepth = $iterator->getDepth();
+ if ($currDepth < $minDepth || ! $this->accept($page)) {
+ // page is not accepted
+ continue;
+ }
+
+ if ($page->isActive(false) && $currDepth > $foundDepth) {
+ // found an active page at a deeper level than before
+ $found = $page;
+ $foundDepth = $currDepth;
+ }
+ }
+
+ if (is_int($maxDepth) && $foundDepth > $maxDepth) {
+ while ($foundDepth > $maxDepth) {
+ if (--$foundDepth < $minDepth) {
+ $found = null;
+ break;
+ }
+
+ $found = $found->getParent();
+ if (! $found instanceof AbstractPage) {
+ $found = null;
+ break;
+ }
+ }
+ }
+
+ if ($found) {
+ return ['page' => $found, 'depth' => $foundDepth];
+ }
+
+ return [];
+ }
+
+ /**
+ * Verifies container and eventually fetches it from service locator if it is a string
+ *
+ * @param AbstractContainer|string|null $container
+ * @return void
+ * @param-out AbstractContainer $container
+ * @throws Exception\InvalidArgumentException
+ */
+ protected function parseContainer(&$container = null)
+ {
+ if (null === $container) {
+ return;
+ }
+
+ if (is_string($container)) {
+ $services = $this->getServiceLocator();
+ if (! $services) {
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'Attempted to set container with alias "%s" but no ServiceLocator was set',
+ $container
+ ));
+ }
+
+ // Fallback
+ if (in_array($container, ['default', 'navigation'], true)) {
+ // Uses class name
+ if ($services->has(Navigation\Navigation::class)) {
+ $container = $services->get(Navigation\Navigation::class);
+ return;
+ }
+
+ // Uses old service name
+ if ($services->has('navigation')) {
+ $container = $services->get('navigation');
+ return;
+ }
+ }
+
+ /**
+ * Load the navigation container from the root service locator
+ */
+ $container = $services->get($container);
+ return;
+ }
+
+ if (! $container instanceof AbstractContainer) {
+ throw new Exception\InvalidArgumentException(
+ 'Container must be a string alias or an instance of '
+ . AbstractContainer::class
+ );
+ }
+ }
+
+ // Iterator filter methods:
+
+ /**
+ * Determines whether a page should be accepted when iterating
+ *
+ * Default listener may be 'overridden' by attaching listener to 'isAllowed'
+ * method. Listener must be 'short circuited' if overriding default ACL
+ * listener.
+ *
+ * Rules:
+ * - If a page is not visible it is not accepted, unless RenderInvisible has
+ * been set to true
+ * - If $useAcl is true (default is true):
+ * - Page is accepted if listener returns true, otherwise false
+ * - If page is accepted and $recursive is true, the page
+ * will not be accepted if it is the descendant of a non-accepted page
+ *
+ * @param AbstractPage $page page to check
+ * @param bool $recursive [optional] if true, page will not be
+ * accepted if it is the descendant of
+ * a page that is not accepted. Default
+ * is true
+ * @return bool Whether page should be accepted
+ */
+ public function accept(AbstractPage $page, $recursive = true)
+ {
+ $accept = true;
+
+ if (! $page->isVisible(false) && ! $this->getRenderInvisible()) {
+ $accept = false;
+ } elseif ($this->getUseAcl()) {
+ $acl = $this->getAcl();
+ $role = $this->getRole();
+ $params = ['acl' => $acl, 'page' => $page, 'role' => $role];
+ $accept = $this->isAllowed($params);
+ }
+
+ if ($accept && $recursive) {
+ $parent = $page->getParent();
+
+ if ($parent instanceof AbstractPage) {
+ $accept = $this->accept($parent, true);
+ }
+ }
+
+ return $accept;
+ }
+
+ /**
+ * Determines whether a page should be allowed given certain parameters
+ *
+ * @param array $params
+ * @return bool
+ */
+ protected function isAllowed($params)
+ {
+ $events = $this->getEventManager() ?: $this->createEventManager();
+ $results = $events->trigger(__FUNCTION__, $this, $params);
+ return $results->last();
+ }
+
+ // Util methods:
+
+ /**
+ * Retrieve whitespace representation of $indent
+ *
+ * @param int|string $indent
+ * @return string
+ */
+ protected function getWhitespace($indent)
+ {
+ if (is_int($indent)) {
+ $indent = str_repeat(' ', $indent);
+ }
+
+ return (string) $indent;
+ }
+
+ /**
+ * Converts an associative array to a string of tag attributes.
+ *
+ * Overloads {@link View\Helper\AbstractHtmlElement::htmlAttribs()}.
+ *
+ * @param array $attribs an array where each key-value pair is converted
+ * to an attribute name and value
+ * @return string
+ */
+ protected function htmlAttribs($attribs)
+ {
+ // filter out null values and empty string values
+ foreach ($attribs as $key => $value) {
+ if ($value === null || (is_string($value) && ! strlen($value))) {
+ unset($attribs[$key]);
+ }
+ }
+
+ return parent::htmlAttribs($attribs);
+ }
+
+ /**
+ * Returns an HTML string containing an 'a' element for the given page
+ *
+ * @param AbstractPage $page page to generate HTML for
+ * @return string HTML string (Label )
+ */
+ public function htmlify(AbstractPage $page)
+ {
+ $label = $this->translate($page->getLabel(), $page->getTextDomain());
+ $title = $this->translate($page->getTitle(), $page->getTextDomain());
+
+ // get attribs for anchor element
+ $attribs = [
+ 'id' => $page->getId(),
+ 'title' => $title,
+ 'class' => $page->getClass(),
+ 'href' => $page->getHref(),
+ 'target' => $page->getTarget(),
+ ];
+
+ if ($page->isActive()) {
+ $attribs['aria-current'] = 'page';
+ }
+
+ /** @var View\Helper\EscapeHtml $escaper */
+ $escaper = $this->view->plugin('escapeHtml');
+ $label = $escaper($label);
+
+ return 'htmlAttribs($attribs) . '>' . $label . ' ';
+ }
+
+ /**
+ * Translate a message (for label, title, …)
+ *
+ * @param string $message ID of the message to translate
+ * @param string $textDomain Text domain (category name for the translations)
+ * @return string Translated message
+ */
+ protected function translate($message, $textDomain = null)
+ {
+ if (! is_string($message) || empty($message)) {
+ return $message;
+ }
+
+ if (! $this->isTranslatorEnabled() || ! $this->hasTranslator()) {
+ return $message;
+ }
+
+ $translator = $this->getTranslator();
+ assert($translator instanceof TranslatorInterface);
+ $textDomain = $textDomain ?: $this->getTranslatorTextDomain();
+
+ return $translator->translate($message, $textDomain);
+ }
+
+ /**
+ * Normalize an ID
+ *
+ * Overrides {@link View\Helper\AbstractHtmlElement::normalizeId()}.
+ *
+ * @param string $value
+ * @return string
+ */
+ protected function normalizeId($value)
+ {
+ $prefix = static::class;
+ $prefix = strtolower(trim(substr($prefix, strrpos($prefix, '\\')), '\\'));
+
+ return $prefix . '-' . $value;
+ }
+
+ /**
+ * Sets ACL to use when iterating pages
+ *
+ * Implements {@link HelperInterface::setAcl()}.
+ *
+ * @param AclInterface $acl ACL object.
+ * @return AbstractHelper
+ */
+ public function setAcl(?AclInterface $acl = null)
+ {
+ $this->acl = $acl;
+ return $this;
+ }
+
+ /**
+ * Returns ACL or null if it isn't set using {@link setAcl()} or
+ * {@link setDefaultAcl()}
+ *
+ * Implements {@link HelperInterface::getAcl()}.
+ *
+ * @return AclInterface|null ACL object or null
+ */
+ public function getAcl()
+ {
+ if ($this->acl === null && static::$defaultAcl !== null) {
+ return static::$defaultAcl;
+ }
+
+ return $this->acl;
+ }
+
+ /**
+ * Checks if the helper has an ACL instance
+ *
+ * Implements {@link HelperInterface::hasAcl()}.
+ *
+ * @return bool
+ */
+ public function hasAcl()
+ {
+ if (
+ $this->acl instanceof Acl\Acl
+ || static::$defaultAcl instanceof Acl\Acl
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set the event manager.
+ *
+ * @return AbstractHelper
+ */
+ public function setEventManager(EventManagerInterface $events)
+ {
+ $events->setIdentifiers([
+ self::class,
+ static::class,
+ ]);
+
+ $this->events = $events;
+
+ if ($events->getSharedManager()) {
+ $this->setDefaultListeners();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the event manager, if present.
+ *
+ * Internally, the helper will lazy-load an EM instance the first time it
+ * requires one, but ideally it should be injected during instantiation.
+ *
+ * @return null|EventManagerInterface
+ */
+ public function getEventManager()
+ {
+ return $this->events;
+ }
+
+ /**
+ * Sets navigation container the helper operates on by default
+ * Implements {@link HelperInterface::setContainer()}.
+ *
+ * @param string|AbstractContainer $container Default is null, meaning container will be reset.
+ * @return AbstractHelper
+ */
+ public function setContainer($container = null)
+ {
+ $this->parseContainer($container);
+ $this->container = $container;
+
+ return $this;
+ }
+
+ /**
+ * Returns the navigation container helper operates on by default
+ * Implements {@link HelperInterface::getContainer()}.
+ * If no container is set, a new container will be instantiated and
+ * stored in the helper.
+ *
+ * @return AbstractContainer navigation container
+ */
+ public function getContainer()
+ {
+ if (null === $this->container) {
+ $this->container = new Navigation\Navigation();
+ }
+
+ return $this->container;
+ }
+
+ /**
+ * Checks if the helper has a container
+ *
+ * Implements {@link HelperInterface::hasContainer()}.
+ *
+ * @return bool
+ */
+ public function hasContainer()
+ {
+ return null !== $this->container;
+ }
+
+ /**
+ * Set the indentation string for using in {@link render()}, optionally a
+ * number of spaces to indent with
+ *
+ * @param string|int $indent
+ * @return AbstractHelper
+ */
+ public function setIndent($indent)
+ {
+ $this->indent = $this->getWhitespace($indent);
+ return $this;
+ }
+
+ /**
+ * Returns indentation
+ *
+ * @return string
+ */
+ public function getIndent()
+ {
+ return $this->indent;
+ }
+
+ /**
+ * Sets the maximum depth a page can have to be included when rendering
+ *
+ * @param int|null|numeric-string $maxDepth Default is null, which sets no maximum depth.
+ * @return AbstractHelper
+ */
+ public function setMaxDepth($maxDepth = null)
+ {
+ if (null === $maxDepth || is_int($maxDepth)) {
+ $this->maxDepth = $maxDepth;
+ } else {
+ $this->maxDepth = (int) $maxDepth;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns maximum depth a page can have to be included when rendering
+ *
+ * @return int|null
+ */
+ public function getMaxDepth()
+ {
+ return $this->maxDepth;
+ }
+
+ /**
+ * Sets the minimum depth a page must have to be included when rendering
+ *
+ * @param int|null|numeric-string $minDepth Default is null, which sets no minimum depth.
+ * @return AbstractHelper
+ */
+ public function setMinDepth($minDepth = null)
+ {
+ if (null === $minDepth || is_int($minDepth)) {
+ $this->minDepth = $minDepth;
+ } else {
+ $this->minDepth = (int) $minDepth;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns minimum depth a page must have to be included when rendering
+ *
+ * @return int|null
+ */
+ public function getMinDepth()
+ {
+ if (! is_int($this->minDepth) || $this->minDepth < 0) {
+ return 0;
+ }
+
+ return $this->minDepth;
+ }
+
+ /**
+ * Render invisible items?
+ *
+ * @param bool $renderInvisible
+ * @return AbstractHelper
+ */
+ public function setRenderInvisible($renderInvisible = true)
+ {
+ $this->renderInvisible = (bool) $renderInvisible;
+ return $this;
+ }
+
+ /**
+ * Return renderInvisible flag
+ *
+ * @return bool
+ */
+ public function getRenderInvisible()
+ {
+ return $this->renderInvisible;
+ }
+
+ /**
+ * Sets ACL role(s) to use when iterating pages
+ *
+ * Implements {@link HelperInterface::setRole()}.
+ *
+ * @param mixed $role [optional] role to set. Expects a string, an
+ * instance of type {@link Acl\Role\RoleInterface}, or null. Default
+ * is null, which will set no role.
+ * @return AbstractHelper
+ * @throws Exception\InvalidArgumentException
+ */
+ public function setRole($role = null)
+ {
+ if (
+ null === $role || is_string($role) ||
+ $role instanceof Acl\Role\RoleInterface
+ ) {
+ $this->role = $role;
+ } else {
+ throw new Exception\InvalidArgumentException(sprintf(
+ '$role must be a string, null, or an instance of '
+ . 'Laminas\Permissions\Role\RoleInterface; %s given',
+ is_object($role) ? $role::class : gettype($role)
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns ACL role to use when iterating pages, or null if it isn't set
+ * using {@link setRole()} or {@link setDefaultRole()}
+ *
+ * Implements {@link HelperInterface::getRole()}.
+ *
+ * @return string|Acl\Role\RoleInterface|null
+ */
+ public function getRole()
+ {
+ if ($this->role === null && static::$defaultRole !== null) {
+ return static::$defaultRole;
+ }
+
+ return $this->role;
+ }
+
+ /**
+ * Checks if the helper has an ACL role
+ *
+ * Implements {@link HelperInterface::hasRole()}.
+ *
+ * @return bool
+ */
+ public function hasRole()
+ {
+ if (
+ $this->role instanceof Acl\Role\RoleInterface
+ || is_string($this->role)
+ || static::$defaultRole instanceof Acl\Role\RoleInterface
+ || is_string(static::$defaultRole)
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set the service locator.
+ *
+ * Used internally to pull named navigation containers to render.
+ *
+ * @return AbstractHelper
+ */
+ public function setServiceLocator(ContainerInterface $serviceLocator)
+ {
+ $this->serviceLocator = $serviceLocator;
+ return $this;
+ }
+
+ /**
+ * Get the service locator.
+ *
+ * Used internally to pull named navigation containers to render.
+ *
+ * @return ContainerInterface|null
+ */
+ public function getServiceLocator()
+ {
+ return $this->serviceLocator;
+ }
+
+ /**
+ * Sets whether ACL should be used
+ *
+ * Implements {@link HelperInterface::setUseAcl()}.
+ *
+ * @param bool $useAcl
+ * @return AbstractHelper
+ */
+ public function setUseAcl($useAcl = true)
+ {
+ $this->useAcl = (bool) $useAcl;
+ return $this;
+ }
+
+ /**
+ * Returns whether ACL should be used
+ *
+ * Implements {@link HelperInterface::getUseAcl()}.
+ *
+ * @return bool
+ */
+ public function getUseAcl()
+ {
+ return $this->useAcl;
+ }
+
+ // Static methods:
+
+ /**
+ * Sets default ACL to use if another ACL is not explicitly set
+ *
+ * @param AclInterface $acl [optional] ACL object. Default is null, which
+ * sets no ACL object.
+ * @return void
+ */
+ public static function setDefaultAcl(?AclInterface $acl = null)
+ {
+ static::$defaultAcl = $acl;
+ }
+
+ /**
+ * Sets default ACL role(s) to use when iterating pages if not explicitly
+ * set later with {@link setRole()}
+ *
+ * @param mixed $role [optional] role to set. Expects null, string, or an
+ * instance of {@link Acl\Role\RoleInterface}. Default is null, which
+ * sets no default role.
+ * @return void
+ * @throws Exception\InvalidArgumentException If role is invalid.
+ */
+ public static function setDefaultRole($role = null)
+ {
+ if (
+ null === $role
+ || is_string($role)
+ || $role instanceof Acl\Role\RoleInterface
+ ) {
+ static::$defaultRole = $role;
+ } else {
+ throw new Exception\InvalidArgumentException(sprintf(
+ '$role must be null|string|Laminas\Permissions\Role\RoleInterface; received "%s"',
+ is_object($role) ? $role::class : gettype($role)
+ ));
+ }
+ }
+
+ /**
+ * Attaches default ACL listeners, if ACLs are in use
+ *
+ * @return void
+ */
+ protected function setDefaultListeners()
+ {
+ if (! $this->getUseAcl()) {
+ return;
+ }
+
+ $events = $this->getEventManager() ?: $this->createEventManager();
+
+ if (! $events->getSharedManager()) {
+ return;
+ }
+
+ $events->getSharedManager()->attach(
+ self::class,
+ 'isAllowed',
+ [AclListener::class, 'accept']
+ );
+ }
+
+ /**
+ * Create and return an event manager instance.
+ *
+ * Ensures that the returned event manager has a shared manager
+ * composed.
+ *
+ * @return EventManager
+ */
+ private function createEventManager()
+ {
+ $r = new ReflectionClass(EventManager::class);
+ if ($r->hasMethod('setSharedManager')) {
+ $events = new EventManager();
+ $events->setSharedManager(new SharedEventManager());
+ } else {
+ $events = new EventManager(new SharedEventManager());
+ }
+
+ $this->setEventManager($events);
+ return $events;
+ }
+}
diff --git a/src/View/Helper/Breadcrumbs.php b/src/View/Helper/Breadcrumbs.php
new file mode 100644
index 00000000..cc058528
--- /dev/null
+++ b/src/View/Helper/Breadcrumbs.php
@@ -0,0 +1,317 @@
+setContainer($container);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Renders helper.
+ *
+ * Implements {@link HelperInterface::render()}.
+ *
+ * @param AbstractContainer $container [optional] container to render. Default is
+ * to render the container registered in the helper.
+ * @return string
+ */
+ public function render($container = null)
+ {
+ $partial = $this->getPartial();
+ if ($partial !== null) {
+ return $this->renderPartial($container, $partial);
+ }
+
+ return $this->renderStraight($container);
+ }
+
+ /**
+ * Renders breadcrumbs by chaining 'a' elements with the separator
+ * registered in the helper.
+ *
+ * @param AbstractContainer $container [optional] container to render. Default is
+ * to render the container registered in the helper.
+ * @return string
+ */
+ public function renderStraight($container = null)
+ {
+ $this->parseContainer($container);
+ if (null === $container) {
+ $container = $this->getContainer();
+ }
+
+ // find deepest active
+ if (! $active = $this->findActive($container)) {
+ return '';
+ }
+
+ $active = $active['page'];
+
+ // put the deepest active page last in breadcrumbs
+ if ($this->getLinkLast()) {
+ $html = $this->htmlify($active);
+ } else {
+ /** @var View\Helper\EscapeHtml $escaper */
+ $escaper = $this->view->plugin('escapeHtml');
+ $label = $escaper(
+ $this->translate($active->getLabel(), $active->getTextDomain())
+ );
+ $attribs = [
+ 'aria-current' => 'page',
+ ];
+ $html = 'htmlAttribs($attribs) . '>' . $label . ' ';
+ }
+
+ // walk back to root
+ while ($parent = $active->getParent()) {
+ if ($parent instanceof AbstractPage) {
+ // prepend crumb to html
+ $html = $this->htmlify($parent)
+ . $this->getSeparator()
+ . $html;
+ }
+
+ if ($parent === $container) {
+ // at the root of the given container
+ break;
+ }
+
+ $active = $parent;
+ }
+
+ return strlen($html) ? $this->getIndent() . $html : '';
+ }
+
+ /**
+ * Renders the given $container by invoking the partial view helper.
+ *
+ * The container will simply be passed on as a model to the view script
+ * as-is, and will be available in the partial script as 'container', e.g.
+ * echo 'Number of pages: ', count($this->container);
.
+ *
+ * @param null|AbstractContainer $container [optional] container to pass to view
+ * script. Default is to use the container registered in the helper.
+ * @param null|string|array $partial [optional] partial view script to use.
+ * Default is to use the partial registered in the helper. If an array
+ * is given, the first value is used for the partial view script.
+ * @return string
+ * @throws Exception\RuntimeException If no partial provided.
+ * @throws Exception\InvalidArgumentException If partial is invalid array.
+ */
+ public function renderPartial($container = null, $partial = null)
+ {
+ return $this->renderPartialModel([], $container, $partial);
+ }
+
+ /**
+ * Renders the given $container by invoking the partial view helper with the given parameters as the model.
+ *
+ * The container will simply be passed on as a model to the view script
+ * as-is, and will be available in the partial script as 'container', e.g.
+ * echo 'Number of pages: ', count($this->container);
.
+ *
+ * Any parameters provided will be passed to the partial via the view model.
+ *
+ * @param null|AbstractContainer $container [optional] container to pass to view
+ * script. Default is to use the container registered in the helper.
+ * @param null|string|array $partial [optional] partial view script to use.
+ * Default is to use the partial registered in the helper. If an array
+ * is given, the first value is used for the partial view script.
+ * @return string
+ * @throws Exception\RuntimeException If no partial provided.
+ * @throws Exception\InvalidArgumentException If partial is invalid array.
+ */
+ public function renderPartialWithParams(array $params = [], $container = null, $partial = null)
+ {
+ return $this->renderPartialModel($params, $container, $partial);
+ }
+
+ /**
+ * Sets whether last page in breadcrumbs should be hyperlinked.
+ *
+ * @param bool $linkLast whether last page should be hyperlinked
+ * @return Breadcrumbs
+ */
+ public function setLinkLast($linkLast)
+ {
+ $this->linkLast = (bool) $linkLast;
+ return $this;
+ }
+
+ /**
+ * Returns whether last page in breadcrumbs should be hyperlinked.
+ *
+ * @return bool
+ */
+ public function getLinkLast()
+ {
+ return $this->linkLast;
+ }
+
+ /**
+ * Sets which partial view script to use for rendering menu.
+ *
+ * @param string|array $partial partial view script or null. If an array is
+ * given, the first value is used for the partial view script.
+ * @return Breadcrumbs
+ */
+ public function setPartial($partial)
+ {
+ if (null === $partial || is_string($partial) || is_array($partial)) {
+ $this->partial = $partial;
+ }
+ return $this;
+ }
+
+ /**
+ * Returns partial view script to use for rendering menu.
+ *
+ * @return string|array|null
+ */
+ public function getPartial()
+ {
+ return $this->partial;
+ }
+
+ /**
+ * Sets breadcrumb separator.
+ *
+ * @param string $separator separator string
+ * @return Breadcrumbs
+ */
+ public function setSeparator($separator)
+ {
+ if (is_string($separator)) {
+ $this->separator = $separator;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns breadcrumb separator.
+ *
+ * @return string breadcrumb separator
+ */
+ public function getSeparator()
+ {
+ return $this->separator;
+ }
+
+ /**
+ * Render a partial with the given "model".
+ *
+ * @param null|AbstractContainer $container
+ * @param null|string|array $partial
+ * @return View\Helper\Partial|string
+ * @throws Exception\RuntimeException If no partial provided.
+ * @throws Exception\InvalidArgumentException If partial is invalid array.
+ */
+ protected function renderPartialModel(array $params, $container, $partial)
+ {
+ $this->parseContainer($container);
+ if (null === $container) {
+ $container = $this->getContainer();
+ }
+ if (null === $partial) {
+ $partial = $this->getPartial();
+ }
+ if (empty($partial)) {
+ throw new Exception\RuntimeException(
+ 'Unable to render breadcrumbs: No partial view script provided'
+ );
+ }
+ $model = array_merge($params, ['pages' => []], ['separator' => $this->getSeparator()]);
+ $active = $this->findActive($container);
+ if ($active) {
+ $active = $active['page'];
+ $model['pages'][] = $active;
+ while ($parent = $active->getParent()) {
+ if (! $parent instanceof AbstractPage) {
+ break;
+ }
+
+ $model['pages'][] = $parent;
+ if ($parent === $container) {
+ // break if at the root of the given container
+ break;
+ }
+ $active = $parent;
+ }
+ $model['pages'] = array_reverse($model['pages']);
+ }
+
+ /** @var View\Helper\Partial $partialHelper */
+ $partialHelper = $this->view->plugin('partial');
+ if (is_array($partial)) {
+ if (count($partial) !== 2) {
+ throw new Exception\InvalidArgumentException(
+ 'Unable to render breadcrumbs: A view partial supplied as '
+ . 'an array must contain one value: the partial view script'
+ );
+ }
+
+ return $partialHelper($partial[0], $model);
+ }
+
+ return $partialHelper($partial, $model);
+ }
+}
diff --git a/src/View/Helper/HelperInterface.php b/src/View/Helper/HelperInterface.php
new file mode 100644
index 00000000..5336d010
--- /dev/null
+++ b/src/View/Helper/HelperInterface.php
@@ -0,0 +1,139 @@
+ elements
+ */
+class Links extends AbstractHelper
+{
+ /**
+ * Constants used for specifying which link types to find and render
+ *
+ * @var int
+ */
+ public const RENDER_ALTERNATE = 0x0001;
+ public const RENDER_STYLESHEET = 0x0002;
+ public const RENDER_START = 0x0004;
+ public const RENDER_NEXT = 0x0008;
+ public const RENDER_PREV = 0x0010;
+ public const RENDER_CONTENTS = 0x0020;
+ public const RENDER_INDEX = 0x0040;
+ public const RENDER_GLOSSARY = 0x0080;
+ public const RENDER_COPYRIGHT = 0x0100;
+ public const RENDER_CHAPTER = 0x0200;
+ public const RENDER_SECTION = 0x0400;
+ public const RENDER_SUBSECTION = 0x0800;
+ public const RENDER_APPENDIX = 0x1000;
+ public const RENDER_HELP = 0x2000;
+ public const RENDER_BOOKMARK = 0x4000;
+ public const RENDER_CUSTOM = 0x8000;
+ public const RENDER_ALL = 0xffff;
+
+ /**
+ * Maps render constants to W3C link types
+ *
+ * @var array
+ */
+ protected static $RELATIONS = [ // phpcs:ignore
+ self::RENDER_ALTERNATE => 'alternate',
+ self::RENDER_STYLESHEET => 'stylesheet',
+ self::RENDER_START => 'start',
+ self::RENDER_NEXT => 'next',
+ self::RENDER_PREV => 'prev',
+ self::RENDER_CONTENTS => 'contents',
+ self::RENDER_INDEX => 'index',
+ self::RENDER_GLOSSARY => 'glossary',
+ self::RENDER_COPYRIGHT => 'copyright',
+ self::RENDER_CHAPTER => 'chapter',
+ self::RENDER_SECTION => 'section',
+ self::RENDER_SUBSECTION => 'subsection',
+ self::RENDER_APPENDIX => 'appendix',
+ self::RENDER_HELP => 'help',
+ self::RENDER_BOOKMARK => 'bookmark',
+ ];
+
+ /**
+ * The helper's render flag
+ *
+ * @see render()
+ * @see setRenderFlag()
+ *
+ * @var int
+ */
+ protected $renderFlag = self::RENDER_ALL;
+
+ /**
+ * Root container
+ *
+ * Used for preventing methods to traverse above the container given to
+ * the {@link render()} method.
+ *
+ * @see _findRoot()
+ *
+ * @var AbstractContainer
+ */
+ protected $root;
+
+ /**
+ * Helper entry point
+ *
+ * @param string|AbstractContainer $container container to operate on
+ * @return Links
+ */
+ public function __invoke($container = null)
+ {
+ if (null !== $container) {
+ $this->setContainer($container);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Magic overload: Proxy calls to {@link findRelation()} or container
+ *
+ * Examples of finder calls:
+ *
+ * // METHOD // SAME AS
+ * $h->findRelNext($page); // $h->findRelation($page, 'rel', 'next')
+ * $h->findRevSection($page); // $h->findRelation($page, 'rev', 'section');
+ * $h->findRelFoo($page); // $h->findRelation($page, 'rel', 'foo');
+ *
+ *
+ * @param string $method
+ * @return mixed
+ * @throws Exception\ExceptionInterface
+ */
+ public function __call($method, array $arguments = [])
+ {
+ ErrorHandler::start(E_WARNING);
+ $result = preg_match('/find(Rel|Rev)(.+)/', $method, $match);
+ ErrorHandler::stop();
+ if ($result) {
+ return $this->findRelation($arguments[0], strtolower($match[1]), strtolower($match[2]));
+ }
+
+ return parent::__call($method, $arguments);
+ }
+
+ /**
+ * Renders helper
+ *
+ * Implements {@link HelperInterface::render()}.
+ *
+ * @param AbstractContainer|string|null $container [optional] container to render.
+ * Default is to render the
+ * container registered in the
+ * helper.
+ * @return string
+ */
+ public function render($container = null)
+ {
+ $this->parseContainer($container);
+ if (null === $container) {
+ $container = $this->getContainer();
+ }
+
+ $active = $this->findActive($container);
+ if ($active) {
+ $active = $active['page'];
+ } else {
+ // no active page
+ return '';
+ }
+
+ $output = '';
+ $indent = $this->getIndent();
+ $this->root = $container;
+
+ $result = $this->findAllRelations($active, $this->getRenderFlag());
+ foreach ($result as $attrib => $types) {
+ foreach ($types as $relation => $pages) {
+ foreach ($pages as $page) {
+ $r = $this->renderLink($page, $attrib, $relation);
+ if ($r) {
+ $output .= $indent . $r . PHP_EOL;
+ }
+ }
+ }
+ }
+
+ $this->root = null;
+
+ // return output (trim last newline by spec)
+ return strlen($output) ? rtrim($output, PHP_EOL) : '';
+ }
+
+ /**
+ * Renders the given $page as a link element, with $attrib = $relation
+ *
+ * @param AbstractPage $page the page to render the link for
+ * @param string $attrib the attribute to use for $type,
+ * either 'rel' or 'rev'
+ * @param string $relation relation type, muse be one of;
+ * alternate, appendix, bookmark,
+ * chapter, contents, copyright,
+ * glossary, help, home, index, next,
+ * prev, section, start, stylesheet,
+ * subsection
+ * @return string
+ * @throws Exception\DomainException
+ */
+ public function renderLink(AbstractPage $page, $attrib, $relation)
+ {
+ if (! in_array($attrib, ['rel', 'rev'])) {
+ throw new Exception\DomainException(sprintf(
+ 'Invalid relation attribute "%s", must be "rel" or "rev"',
+ $attrib
+ ));
+ }
+
+ if (! $href = $page->getHref()) {
+ return '';
+ }
+
+ // TODO: add more attribs
+ // http://www.w3.org/TR/html401/struct/links.html#h-12.2
+ $attribs = [
+ $attrib => $relation,
+ 'href' => $href,
+ 'title' => $page->getLabel(),
+ ];
+
+ return ' htmlAttribs($attribs)
+ . $this->getClosingBracket();
+ }
+
+ // Finder methods:
+
+ /**
+ * Finds all relations (forward and reverse) for the given $page
+ *
+ * The form of the returned array:
+ *
+ *
+ * // $page denotes an instance of Laminas\Navigation\Page\AbstractPage
+ * $returned = array(
+ * 'rel' => array(
+ * 'alternate' => array($page, $page, $page),
+ * 'start' => array($page),
+ * 'next' => array($page),
+ * 'prev' => array($page),
+ * 'canonical' => array($page)
+ * ),
+ * 'rev' => array(
+ * 'section' => array($page)
+ * )
+ * );
+ *
+ *
+ * @param AbstractPage $page page to find links for
+ * @param int|null $flag
+ * @return array[][]
+ * @psalm-return array{rel: array, rev: array}
+ */
+ public function findAllRelations(AbstractPage $page, $flag = null)
+ {
+ if (! is_int($flag)) {
+ $flag = self::RENDER_ALL;
+ }
+
+ $result = ['rel' => [], 'rev' => []];
+ $native = array_values(static::$RELATIONS);
+
+ foreach (array_keys($result) as $rel) {
+ $meth = 'getDefined' . ucfirst($rel);
+ $types = array_merge($native, array_diff($page->$meth(), $native));
+
+ foreach ($types as $type) {
+ if (! $relFlag = array_search($type, static::$RELATIONS)) {
+ $relFlag = self::RENDER_CUSTOM;
+ }
+ if (! ($flag & $relFlag)) {
+ continue;
+ }
+
+ $found = $this->findRelation($page, $rel, $type);
+ if ($found) {
+ if (! is_array($found)) {
+ $found = [$found];
+ }
+ $result[$rel][$type] = $found;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Finds relations of the given $rel=$type from $page
+ *
+ * This method will first look for relations in the page instance, then
+ * by searching the root container if nothing was found in the page.
+ *
+ * @param AbstractPage $page page to find relations for
+ * @param string $rel relation, "rel" or "rev"
+ * @param string $type link type, e.g. 'start', 'next'
+ * @return AbstractPage|array|null
+ * @throws Exception\DomainException If $rel is not "rel" or "rev".
+ */
+ public function findRelation(AbstractPage $page, $rel, $type)
+ {
+ if (! in_array($rel, ['rel', 'rev'])) {
+ throw new Exception\DomainException(sprintf(
+ 'Invalid argument: $rel must be "rel" or "rev"; "%s" given',
+ $rel
+ ));
+ }
+
+ if (! $result = $this->findFromProperty($page, $rel, $type)) {
+ $result = $this->findFromSearch($page, $rel, $type);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Finds relations of given $type for $page by checking if the
+ * relation is specified as a property of $page
+ *
+ * @param AbstractPage $page page to find relations for
+ * @param string $rel relation, 'rel' or 'rev'
+ * @param string $type link type, e.g. 'start', 'next'
+ * @return AbstractPage|array|null
+ */
+ protected function findFromProperty(AbstractPage $page, $rel, $type)
+ {
+ $method = 'get' . ucfirst($rel);
+ $result = $page->$method($type);
+ if ($result) {
+ $result = $this->convertToPages($result);
+ if ($result) {
+ if (! is_array($result)) {
+ $result = [$result];
+ }
+
+ foreach ($result as $key => $page) {
+ if (! $this->accept($page)) {
+ unset($result[$key]);
+ }
+ }
+
+ return count($result) === 1 ? $result[0] : $result;
+ }
+ }
+ }
+
+ /**
+ * Finds relations of given $rel=$type for $page by using the helper to
+ * search for the relation in the root container
+ *
+ * @param AbstractPage $page page to find relations for
+ * @param string $rel relation, 'rel' or 'rev'
+ * @param string $type link type, e.g. 'start', 'next', etc
+ * @return array|null
+ */
+ protected function findFromSearch(AbstractPage $page, $rel, $type)
+ {
+ $found = null;
+
+ $method = 'search' . ucfirst($rel) . ucfirst($type);
+ if (method_exists($this, $method)) {
+ $found = $this->$method($page);
+ }
+
+ return $found;
+ }
+
+ // Search methods:
+
+ /**
+ * Searches the root container for the forward 'start' relation of the given
+ * $page
+ *
+ * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+ * Refers to the first document in a collection of documents. This link type
+ * tells search engines which document is considered by the author to be the
+ * starting point of the collection.
+ *
+ * @return AbstractPage|null
+ */
+ public function searchRelStart(AbstractPage $page)
+ {
+ $found = $this->findRoot($page);
+ if (! $found instanceof AbstractPage) {
+ $found->rewind();
+ $found = $found->current();
+ }
+
+ if ($found === $page || ! $this->accept($found)) {
+ $found = null;
+ }
+
+ return $found;
+ }
+
+ /**
+ * Searches the root container for the forward 'next' relation of the given
+ * $page
+ *
+ * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+ * Refers to the next document in a linear sequence of documents. User
+ * agents may choose to preload the "next" document, to reduce the perceived
+ * load time.
+ *
+ * @return AbstractPage|null
+ */
+ public function searchRelNext(AbstractPage $page)
+ {
+ $found = null;
+ $break = false;
+ $iterator = new RecursiveIteratorIterator($this->findRoot($page), RecursiveIteratorIterator::SELF_FIRST);
+ foreach ($iterator as $intermediate) {
+ if ($intermediate === $page) {
+ // current page; break at next accepted page
+ $break = true;
+ continue;
+ }
+
+ if ($break && $this->accept($intermediate)) {
+ $found = $intermediate;
+ break;
+ }
+ }
+
+ return $found;
+ }
+
+ /**
+ * Searches the root container for the forward 'prev' relation of the given
+ * $page
+ *
+ * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+ * Refers to the previous document in an ordered series of documents. Some
+ * user agents also support the synonym "Previous".
+ *
+ * @return AbstractPage|null
+ */
+ public function searchRelPrev(AbstractPage $page)
+ {
+ $found = null;
+ $prev = null;
+ $iterator = new RecursiveIteratorIterator(
+ $this->findRoot($page),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+ foreach ($iterator as $intermediate) {
+ if (! $this->accept($intermediate)) {
+ continue;
+ }
+ if ($intermediate === $page) {
+ $found = $prev;
+ break;
+ }
+
+ $prev = $intermediate;
+ }
+
+ return $found;
+ }
+
+ /**
+ * Searches the root container for forward 'chapter' relations of the given
+ * $page
+ *
+ * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+ * Refers to a document serving as a chapter in a collection of documents.
+ *
+ * @return AbstractPage|array|null
+ */
+ public function searchRelChapter(AbstractPage $page)
+ {
+ $found = [];
+
+ // find first level of pages
+ $root = $this->findRoot($page);
+
+ // find start page(s)
+ $start = $this->findRelation($page, 'rel', 'start');
+ if (! is_array($start)) {
+ $start = [$start];
+ }
+
+ foreach ($root as $chapter) {
+ // exclude self and start page from chapters
+ if (
+ $chapter !== $page &&
+ ! in_array($chapter, $start) &&
+ $this->accept($chapter)
+ ) {
+ $found[] = $chapter;
+ }
+ }
+
+ switch (count($found)) {
+ case 0:
+ return;
+ case 1:
+ return $found[0];
+ default:
+ return $found;
+ }
+ }
+
+ /**
+ * Searches the root container for forward 'section' relations of the given
+ * $page
+ *
+ * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+ * Refers to a document serving as a section in a collection of documents.
+ *
+ * @return AbstractPage|array|null
+ */
+ public function searchRelSection(AbstractPage $page)
+ {
+ $found = [];
+
+ // check if given page has pages and is a chapter page
+ if ($page->hasPages() && $this->findRoot($page)->hasPage($page)) {
+ foreach ($page as $section) {
+ if ($this->accept($section)) {
+ $found[] = $section;
+ }
+ }
+ }
+
+ switch (count($found)) {
+ case 0:
+ return;
+ case 1:
+ return $found[0];
+ default:
+ return $found;
+ }
+ }
+
+ /**
+ * Searches the root container for forward 'subsection' relations of the
+ * given $page
+ *
+ * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+ * Refers to a document serving as a subsection in a collection of
+ * documents.
+ *
+ * @return AbstractPage|array|null
+ */
+ public function searchRelSubsection(AbstractPage $page)
+ {
+ $found = [];
+
+ if ($page->hasPages()) {
+ // given page has child pages, loop chapters
+ foreach ($this->findRoot($page) as $chapter) {
+ // is page a section?
+ if ($chapter->hasPage($page)) {
+ foreach ($page as $subsection) {
+ if ($this->accept($subsection)) {
+ $found[] = $subsection;
+ }
+ }
+ }
+ }
+ }
+
+ switch (count($found)) {
+ case 0:
+ return;
+ case 1:
+ return $found[0];
+ default:
+ return $found;
+ }
+ }
+
+ /**
+ * Searches the root container for the reverse 'section' relation of the
+ * given $page
+ *
+ * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+ * Refers to a document serving as a section in a collection of documents.
+ *
+ * @return AbstractPage|null
+ */
+ public function searchRevSection(AbstractPage $page)
+ {
+ $found = null;
+ $parent = $page->getParent();
+ if ($parent) {
+ if (
+ $parent instanceof AbstractPage &&
+ $this->findRoot($page)->hasPage($parent)
+ ) {
+ $found = $parent;
+ }
+ }
+
+ return $found;
+ }
+
+ /**
+ * Searches the root container for the reverse 'section' relation of the
+ * given $page
+ *
+ * From {@link http://www.w3.org/TR/html4/types.html#type-links}:
+ * Refers to a document serving as a subsection in a collection of
+ * documents.
+ *
+ * @return AbstractPage|null
+ */
+ public function searchRevSubsection(AbstractPage $page)
+ {
+ $found = null;
+ $parent = $page->getParent();
+ if ($parent) {
+ if ($parent instanceof AbstractPage) {
+ $root = $this->findRoot($page);
+ foreach ($root as $chapter) {
+ if ($chapter->hasPage($parent)) {
+ $found = $parent;
+ break;
+ }
+ }
+ }
+ }
+
+ return $found;
+ }
+
+ // Util methods:
+
+ /**
+ * Returns the root container of the given page
+ *
+ * When rendering a container, the render method still store the given
+ * container as the root container, and unset it when done rendering. This
+ * makes sure finder methods will not traverse above the container given
+ * to the render method.
+ *
+ * @return AbstractContainer
+ */
+ protected function findRoot(AbstractPage $page)
+ {
+ if ($this->root) {
+ return $this->root;
+ }
+
+ $root = $page;
+
+ while ($parent = $page->getParent()) {
+ $root = $parent;
+ if ($parent instanceof AbstractPage) {
+ $page = $parent;
+ } else {
+ break;
+ }
+ }
+
+ return $root;
+ }
+
+ /**
+ * Converts a $mixed value to an array of pages
+ *
+ * @param mixed $mixed mixed value to get page(s) from
+ * @param bool $recursive whether $value should be looped
+ * if it is an array or a config
+ * @return AbstractPage|array|null
+ */
+ protected function convertToPages($mixed, $recursive = true)
+ {
+ if ($mixed instanceof AbstractPage) {
+ // value is a page instance; return directly
+ return $mixed;
+ } elseif ($mixed instanceof AbstractContainer) {
+ // value is a container; return pages in it
+ $pages = [];
+ foreach ($mixed as $page) {
+ $pages[] = $page;
+ }
+ return $pages;
+ } elseif ($mixed instanceof Traversable) {
+ $mixed = ArrayUtils::iteratorToArray($mixed);
+ } elseif (is_string($mixed)) {
+ // value is a string; make a URI page
+ return AbstractPage::factory([
+ 'type' => 'uri',
+ 'uri' => $mixed,
+ ]);
+ }
+
+ if (is_array($mixed) && ! empty($mixed)) {
+ if ($recursive && is_numeric(key($mixed))) {
+ // first key is numeric; assume several pages
+ $pages = [];
+ foreach ($mixed as $value) {
+ $value = $this->convertToPages($value, false);
+ if ($value) {
+ $pages[] = $value;
+ }
+ }
+ return $pages;
+ } else {
+ // pass array to factory directly
+ try {
+ return AbstractPage::factory($mixed);
+ } catch (\Exception $e) {
+ }
+ }
+ }
+
+ // nothing found
+ }
+
+ /**
+ * Sets the helper's render flag
+ *
+ * The helper uses the bitwise '&' operator against the hex values of the
+ * render constants. This means that the flag can is "bitwised" value of
+ * the render constants. Examples:
+ *
+ * // render all links except glossary
+ * $flag = Links:RENDER_ALL ^ Links:RENDER_GLOSSARY;
+ * $helper->setRenderFlag($flag);
+ *
+ * // render only chapters and sections
+ * $flag = Links:RENDER_CHAPTER | Links:RENDER_SECTION;
+ * $helper->setRenderFlag($flag);
+ *
+ * // render only relations that are not native W3C relations
+ * $helper->setRenderFlag(Links:RENDER_CUSTOM);
+ *
+ * // render all relations (default)
+ * $helper->setRenderFlag(Links:RENDER_ALL);
+ *
+ *
+ * Note that custom relations can also be rendered directly using the
+ * {@link renderLink()} method.
+ *
+ * @param int $renderFlag
+ * @return Links
+ */
+ public function setRenderFlag($renderFlag)
+ {
+ $this->renderFlag = (int) $renderFlag;
+
+ return $this;
+ }
+
+ /**
+ * Returns the helper's render flag
+ *
+ * @return int
+ */
+ public function getRenderFlag()
+ {
+ return $this->renderFlag;
+ }
+}
diff --git a/src/View/Helper/Listener/AclListener.php b/src/View/Helper/Listener/AclListener.php
new file mode 100644
index 00000000..b7bcf482
--- /dev/null
+++ b/src/View/Helper/Listener/AclListener.php
@@ -0,0 +1,55 @@
+getParams();
+ $acl = $params['acl'] ?? null;
+ $page = $params['page'] ?? null;
+ $role = $params['role'] ?? null;
+
+ if (! $acl instanceof Acl) {
+ return true;
+ }
+
+ assert($page instanceof AbstractPage);
+ assert($role instanceof RoleInterface || is_string($role) || $role === null);
+
+ $resource = $page->getResource();
+ $privilege = $page->getPrivilege();
+
+ if ($resource !== null) {
+ $accepted = $acl->hasResource($resource)
+ && $acl->isAllowed($role, $resource, $privilege);
+ }
+
+ return $accepted;
+ }
+}
diff --git a/src/View/Helper/Menu.php b/src/View/Helper/Menu.php
new file mode 100644
index 00000000..edcf822c
--- /dev/null
+++ b/src/View/Helper/Menu.php
@@ -0,0 +1,796 @@
+ element.
+ *
+ * @var bool
+ */
+ protected $addClassToListItem = false;
+
+ /**
+ * Whether labels should be escaped.
+ *
+ * @var bool
+ */
+ protected $escapeLabels = true;
+
+ /**
+ * Whether only active branch should be rendered.
+ *
+ * @var bool
+ */
+ protected $onlyActiveBranch = false;
+
+ /**
+ * Partial view script to use for rendering menu.
+ *
+ * @var string|array
+ */
+ protected $partial;
+
+ /**
+ * Whether parents should be rendered when only rendering active branch.
+ *
+ * @var bool
+ */
+ protected $renderParents = true;
+
+ /**
+ * CSS class to use for the ul element.
+ *
+ * @var string
+ */
+ protected $ulClass = 'navigation';
+
+ /**
+ * CSS class to use for the active li element.
+ *
+ * @var string
+ */
+ protected $liActiveClass = 'active';
+
+ /**
+ * View helper entry point.
+ *
+ * Retrieves helper and optionally sets container to operate on.
+ *
+ * @param AbstractContainer $container [optional] container to operate on
+ * @return self
+ */
+ public function __invoke($container = null)
+ {
+ if (null !== $container) {
+ $this->setContainer($container);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Renders menu.
+ *
+ * Implements {@link HelperInterface::render()}.
+ *
+ * If a partial view is registered in the helper, the menu will be rendered
+ * using the given partial script. If no partial is registered, the menu
+ * will be rendered as an 'ul' element by the helper's internal method.
+ *
+ * @see renderPartial()
+ * @see renderMenu()
+ *
+ * @param AbstractContainer $container [optional] container to render. Default is
+ * to render the container registered in the helper.
+ * @return string
+ */
+ public function render($container = null)
+ {
+ $partial = $this->getPartial();
+ if ($partial) {
+ return $this->renderPartial($container, $partial);
+ }
+
+ return $this->renderMenu($container);
+ }
+
+ /**
+ * Renders the deepest active menu within [$minDepth, $maxDepth], (called from {@link renderMenu()}).
+ *
+ * @param AbstractContainer $container container to render
+ * @param string $ulClass CSS class for first UL
+ * @param string $indent initial indentation
+ * @param int|null $minDepth minimum depth
+ * @param int|null $maxDepth maximum depth
+ * @param bool $escapeLabels Whether or not to escape the labels
+ * @param bool $addClassToListItem Whether or not page class applied to element
+ * @param string $liActiveClass CSS class for active LI
+ */
+ protected function renderDeepestMenu(
+ AbstractContainer $container,
+ $ulClass,
+ $indent,
+ $minDepth,
+ $maxDepth,
+ $escapeLabels,
+ $addClassToListItem,
+ $liActiveClass
+ ): string {
+ if (! $active = $this->findActive($container, $minDepth - 1, $maxDepth)) {
+ return '';
+ }
+
+ // special case if active page is one below minDepth
+ if ($active['depth'] < $minDepth) {
+ if (! $active['page']->hasPages(! $this->renderInvisible)) {
+ return '';
+ }
+ } elseif (! $active['page']->hasPages(! $this->renderInvisible)) {
+ // found pages has no children; render siblings
+ $active['page'] = $active['page']->getParent();
+ } elseif (is_int($maxDepth) && $active['depth'] + 1 > $maxDepth) {
+ // children are below max depth; render siblings
+ $active['page'] = $active['page']->getParent();
+ }
+
+ $escaper = $this->view->plugin('escapeHtmlAttr');
+ assert($escaper instanceof EscapeHtmlAttr);
+ $ulClass = $ulClass ? ' class="' . $escaper($ulClass) . '"' : '';
+ $html = $indent . '' . PHP_EOL;
+
+ foreach ($active['page'] as $subPage) {
+ if (! $this->accept($subPage)) {
+ continue;
+ }
+
+ // render li tag and page
+ $liClasses = [];
+
+ // Is page active?
+ if ($subPage->isActive(true)) {
+ $liClasses[] = $liActiveClass;
+ }
+
+ // Add CSS class from page to
+ if ($addClassToListItem && $subPage->getClass()) {
+ $liClasses[] = $subPage->getClass();
+ }
+
+ $liClass = empty($liClasses) ? '' : ' class="' . $escaper(implode(' ', $liClasses)) . '"';
+ $html .= $indent . ' ' . PHP_EOL;
+ $html .= $indent . ' ' . $this->htmlify($subPage, $escapeLabels, $addClassToListItem) . PHP_EOL;
+ $html .= $indent . ' ' . PHP_EOL;
+ }
+
+ $html .= $indent . ' ';
+
+ return $html;
+ }
+
+ /**
+ * Renders helper.
+ *
+ * Renders a HTML 'ul' for the given $container. If $container is not given,
+ * the container registered in the helper will be used.
+ *
+ * Available $options:
+ *
+ * @param AbstractContainer $container [optional] container to create menu from.
+ * Default is to use the container retrieved from {@link getContainer()}.
+ * @param array $options [optional] options for controlling rendering
+ */
+ public function renderMenu($container = null, array $options = []): string
+ {
+ $this->parseContainer($container);
+ if (null === $container) {
+ $container = $this->getContainer();
+ }
+
+ $options = $this->normalizeOptions($options);
+ if ($options['onlyActiveBranch'] && ! $options['renderParents']) {
+ return $this->renderDeepestMenu(
+ $container,
+ $options['ulClass'],
+ $options['indent'],
+ $options['minDepth'],
+ $options['maxDepth'],
+ $options['escapeLabels'],
+ $options['addClassToListItem'],
+ $options['liActiveClass']
+ );
+ }
+
+ return $this->renderNormalMenu(
+ $container,
+ $options['ulClass'],
+ $options['indent'],
+ $options['minDepth'],
+ $options['maxDepth'],
+ $options['onlyActiveBranch'],
+ $options['escapeLabels'],
+ $options['addClassToListItem'],
+ $options['liActiveClass']
+ );
+ }
+
+ /**
+ * Renders a normal menu (called from {@link renderMenu()}).
+ *
+ * @param AbstractContainer $container container to render
+ * @param string $ulClass CSS class for first UL
+ * @param string $indent initial indentation
+ * @param int|null $minDepth minimum depth
+ * @param int|null $maxDepth maximum depth
+ * @param bool $onlyActive render only active branch?
+ * @param bool $escapeLabels Whether or not to escape the labels
+ * @param bool $addClassToListItem Whether or not page class applied to element
+ * @param string $liActiveClass CSS class for active LI
+ */
+ protected function renderNormalMenu(
+ AbstractContainer $container,
+ $ulClass,
+ $indent,
+ $minDepth,
+ $maxDepth,
+ $onlyActive,
+ $escapeLabels,
+ $addClassToListItem,
+ $liActiveClass
+ ): string {
+ $html = '';
+
+ // find deepest active
+ $found = $this->findActive($container, $minDepth, $maxDepth);
+
+ $escaper = $this->view->plugin('escapeHtmlAttr');
+ assert($escaper instanceof EscapeHtmlAttr);
+
+ $foundPage = null;
+ $foundDepth = 0;
+
+ if ($found) {
+ $foundPage = $found['page'];
+ $foundDepth = $found['depth'];
+ }
+
+ // create iterator
+ $iterator = new RecursiveIteratorIterator(
+ $container,
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ if (is_int($maxDepth)) {
+ $iterator->setMaxDepth($maxDepth);
+ }
+
+ // iterate container
+ $prevDepth = -1;
+ foreach ($iterator as $page) {
+ $depth = $iterator->getDepth();
+ $isActive = $page->isActive(true);
+ if ($depth < $minDepth || ! $this->accept($page)) {
+ // page is below minDepth or not accepted by acl/visibility
+ continue;
+ } elseif ($onlyActive && ! $isActive) {
+ // page is not active itself, but might be in the active branch
+ $accept = false;
+ if ($foundPage) {
+ if ($foundPage->hasPage($page)) {
+ // accept if page is a direct child of the active page
+ $accept = true;
+ } elseif ($foundPage->getParent()->hasPage($page)) {
+ // page is a sibling of the active page...
+ if (
+ ! $foundPage->hasPages(! $this->renderInvisible)
+ || is_int($maxDepth) && $foundDepth + 1 > $maxDepth
+ ) {
+ // accept if active page has no children, or the
+ // children are too deep to be rendered
+ $accept = true;
+ }
+ }
+ }
+ if (! $accept) {
+ continue;
+ }
+ }
+
+ // make sure indentation is correct
+ $depth -= $minDepth;
+ $myIndent = $indent . str_repeat(' ', $depth);
+ if ($depth > $prevDepth) {
+ // start new ul tag
+ if ($ulClass && $depth === 0) {
+ $ulClass = ' class="' . $escaper($ulClass) . '"';
+ } else {
+ $ulClass = '';
+ }
+ $html .= $myIndent . '' . PHP_EOL;
+ } elseif ($prevDepth > $depth) {
+ // close li/ul tags until we're at current depth
+ for ($i = $prevDepth; $i > $depth; $i--) {
+ $ind = $indent . str_repeat(' ', $i);
+ $html .= $ind . ' ' . PHP_EOL;
+ $html .= $ind . '' . PHP_EOL;
+ }
+ // close previous li tag
+ $html .= $myIndent . ' ' . PHP_EOL;
+ } else {
+ // close previous li tag
+ $html .= $myIndent . ' ' . PHP_EOL;
+ }
+
+ // render li tag and page
+ $liClasses = [];
+
+ // Is page active?
+ if ($isActive) {
+ $liClasses[] = $liActiveClass;
+ }
+
+ // Add CSS class from page to
+ if ($addClassToListItem && $page->getClass()) {
+ $liClasses[] = $page->getClass();
+ }
+ $liClass = empty($liClasses) ? '' : ' class="' . $escaper(implode(' ', $liClasses)) . '"';
+ $html .= $myIndent . ' ' . PHP_EOL
+ . $myIndent . ' ' . $this->htmlify($page, $escapeLabels, $addClassToListItem) . PHP_EOL;
+
+ // store as previous depth for next iteration
+ $prevDepth = $depth;
+ }
+
+ if ($html) {
+ // done iterating container; close open ul/li tags
+ for ($i = $prevDepth + 1; $i > 0; $i--) {
+ $myIndent = $indent . str_repeat(' ', $i - 1);
+ $html .= $myIndent . ' ' . PHP_EOL
+ . $myIndent . '' . PHP_EOL;
+ }
+ $html = rtrim($html, PHP_EOL);
+ }
+
+ return $html;
+ }
+
+ /**
+ * Renders the given $container by invoking the partial view helper.
+ *
+ * The container will simply be passed on as a model to the view script
+ * as-is, and will be available in the partial script as 'container', e.g.
+ * echo 'Number of pages: ', count($this->container);
.
+ *
+ * @param null|AbstractContainer $container [optional] container to pass to view
+ * script. Default is to use the container registered in the helper.
+ * @param null|string|array $partial [optional] partial view script to use.
+ * Default is to use the partial registered in the helper. If an array
+ * is given, the first value is used for the partial view script.
+ * @return string
+ * @throws Exception\RuntimeException If no partial provided.
+ * @throws Exception\InvalidArgumentException If partial is invalid array.
+ */
+ public function renderPartial($container = null, $partial = null)
+ {
+ return $this->renderPartialModel([], $container, $partial);
+ }
+
+ /**
+ * Renders the given $container by invoking the partial view helper with the given parameters as the model.
+ *
+ * The container will simply be passed on as a model to the view script
+ * as-is, and will be available in the partial script as 'container', e.g.
+ * echo 'Number of pages: ', count($this->container);
.
+ *
+ * Any parameters provided will be passed to the partial via the view model.
+ *
+ * @param null|AbstractContainer $container [optional] container to pass to view
+ * script. Default is to use the container registered in the helper.
+ * @param null|string|array $partial [optional] partial view script to use.
+ * Default is to use the partial registered in the helper. If an array
+ * is given, the first value is used for the partial view script.
+ * @return string
+ * @throws Exception\RuntimeException If no partial provided.
+ * @throws Exception\InvalidArgumentException If partial is invalid array.
+ */
+ public function renderPartialWithParams(array $params = [], $container = null, $partial = null)
+ {
+ return $this->renderPartialModel($params, $container, $partial);
+ }
+
+ /**
+ * Renders the inner-most sub menu for the active page in the $container.
+ *
+ * This is a convenience method which is equivalent to the following call:
+ *
+ * renderMenu($container, array(
+ * 'indent' => $indent,
+ * 'ulClass' => $ulClass,
+ * 'minDepth' => null,
+ * 'maxDepth' => null,
+ * 'onlyActiveBranch' => true,
+ * 'renderParents' => false,
+ * 'liActiveClass' => $liActiveClass
+ * ));
+ *
+ *
+ * @param AbstractContainer|null $container [optional] container to render.
+ * Default is to render the container registered in the helper.
+ * @param string $ulClass [optional] CSS class to use for UL element.
+ * Default is to use the value from {@link getUlClass()}.
+ * @param string|int $indent [optional] indentation as a string or number
+ * of spaces. Default is to use the value retrieved from
+ * {@link getIndent()}.
+ * @param string $liActiveClass [optional] CSS class to use for UL
+ * element. Default is to use the value from {@link getUlClass()}.
+ * @return string
+ */
+ public function renderSubMenu(
+ ?AbstractContainer $container = null,
+ $ulClass = null,
+ $indent = null,
+ $liActiveClass = null
+ ) {
+ return $this->renderMenu($container, [
+ 'indent' => $indent,
+ 'ulClass' => $ulClass,
+ 'minDepth' => null,
+ 'maxDepth' => null,
+ 'onlyActiveBranch' => true,
+ 'renderParents' => false,
+ 'escapeLabels' => true,
+ 'addClassToListItem' => false,
+ 'liActiveClass' => $liActiveClass,
+ ]);
+ }
+
+ /**
+ * Returns an HTML string containing an 'a' element for the given page if
+ * the page's href is not empty, and a 'span' element if it is empty.
+ *
+ * Overrides {@link AbstractHelper::htmlify()}.
+ *
+ * @param AbstractPage $page page to generate HTML for
+ * @param bool $escapeLabel Whether or not to escape the label
+ * @param bool $addClassToListItem Whether or not to add the page class to the list item
+ * @return string
+ */
+ public function htmlify(AbstractPage $page, $escapeLabel = true, $addClassToListItem = false)
+ {
+ // get attribs for element
+ $attribs = [
+ 'id' => $page->getId(),
+ 'title' => $this->translate($page->getTitle(), $page->getTextDomain()),
+ ];
+
+ if ($addClassToListItem === false) {
+ $attribs['class'] = $page->getClass();
+ }
+
+ // does page have a href?
+ $href = $page->getHref();
+ if ($href) {
+ $element = 'a';
+ $attribs['href'] = $href;
+ $attribs['target'] = $page->getTarget();
+ } else {
+ $element = 'span';
+ }
+
+ if ($page->isActive()) {
+ $attribs['aria-current'] = 'page';
+ }
+
+ $html = '<' . $element . $this->htmlAttribs($attribs) . '>';
+ $label = $this->translate($page->getLabel(), $page->getTextDomain());
+
+ if ($escapeLabel === true) {
+ /** @var EscapeHtml $escaper */
+ $escaper = $this->view->plugin('escapeHtml');
+ $html .= $escaper($label);
+ } else {
+ $html .= $label;
+ }
+
+ $html .= '' . $element . '>';
+ return $html;
+ }
+
+ /**
+ * Normalizes given render options.
+ *
+ * @param array $options [optional] options to normalize
+ * @return array
+ */
+ protected function normalizeOptions(array $options = [])
+ {
+ if (isset($options['indent'])) {
+ $options['indent'] = $this->getWhitespace($options['indent']);
+ } else {
+ $options['indent'] = $this->getIndent();
+ }
+
+ if (isset($options['ulClass']) && $options['ulClass'] !== null) {
+ $options['ulClass'] = (string) $options['ulClass'];
+ } else {
+ $options['ulClass'] = $this->getUlClass();
+ }
+
+ if (array_key_exists('minDepth', $options)) {
+ if (null !== $options['minDepth']) {
+ $options['minDepth'] = (int) $options['minDepth'];
+ }
+ } else {
+ $options['minDepth'] = $this->getMinDepth();
+ }
+
+ if ($options['minDepth'] < 0 || $options['minDepth'] === null) {
+ $options['minDepth'] = 0;
+ }
+
+ if (array_key_exists('maxDepth', $options)) {
+ if (null !== $options['maxDepth']) {
+ $options['maxDepth'] = (int) $options['maxDepth'];
+ }
+ } else {
+ $options['maxDepth'] = $this->getMaxDepth();
+ }
+
+ if (! isset($options['onlyActiveBranch'])) {
+ $options['onlyActiveBranch'] = $this->getOnlyActiveBranch();
+ }
+
+ if (! isset($options['escapeLabels'])) {
+ $options['escapeLabels'] = $this->escapeLabels;
+ }
+
+ if (! isset($options['renderParents'])) {
+ $options['renderParents'] = $this->getRenderParents();
+ }
+
+ if (! isset($options['addClassToListItem'])) {
+ $options['addClassToListItem'] = $this->getAddClassToListItem();
+ }
+
+ if (isset($options['liActiveClass']) && $options['liActiveClass'] !== null) {
+ $options['liActiveClass'] = (string) $options['liActiveClass'];
+ } else {
+ $options['liActiveClass'] = $this->getLiActiveClass();
+ }
+
+ return $options;
+ }
+
+ /**
+ * Sets a flag indicating whether labels should be escaped.
+ *
+ * @param bool $flag [optional] escape labels
+ * @return self
+ */
+ public function escapeLabels($flag = true)
+ {
+ $this->escapeLabels = (bool) $flag;
+ return $this;
+ }
+
+ /**
+ * Enables/disables page class applied to element.
+ *
+ * @param bool $flag [optional] page class applied to element Default
+ * is true.
+ * @return self fluent interface, returns self
+ */
+ public function setAddClassToListItem($flag = true)
+ {
+ $this->addClassToListItem = (bool) $flag;
+ return $this;
+ }
+
+ /**
+ * Returns flag indicating whether page class should be applied to element.
+ *
+ * By default, this value is false.
+ *
+ * @return bool whether parents should be rendered
+ */
+ public function getAddClassToListItem()
+ {
+ return $this->addClassToListItem;
+ }
+
+ /**
+ * Sets a flag indicating whether only active branch should be rendered.
+ *
+ * @param bool $flag [optional] render only active branch.
+ * @return self
+ */
+ public function setOnlyActiveBranch($flag = true)
+ {
+ $this->onlyActiveBranch = (bool) $flag;
+ return $this;
+ }
+
+ /**
+ * Returns a flag indicating whether only active branch should be rendered.
+ *
+ * By default, this value is false, meaning the entire menu will be
+ * be rendered.
+ *
+ * @return bool
+ */
+ public function getOnlyActiveBranch()
+ {
+ return $this->onlyActiveBranch;
+ }
+
+ /**
+ * Sets which partial view script to use for rendering menu.
+ *
+ * @param string|array $partial partial view script or null. If an array
+ * is given, the first value is used for the partial view script.
+ * @return self
+ */
+ public function setPartial($partial)
+ {
+ if (null === $partial || is_string($partial) || is_array($partial)) {
+ $this->partial = $partial;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns partial view script to use for rendering menu.
+ *
+ * @return string|array|null
+ */
+ public function getPartial()
+ {
+ return $this->partial;
+ }
+
+ /**
+ * Enables/disables rendering of parents when only rendering active branch.
+ *
+ * See {@link setOnlyActiveBranch()} for more information.
+ *
+ * @param bool $flag [optional] render parents when rendering active branch.
+ * @return self
+ */
+ public function setRenderParents($flag = true)
+ {
+ $this->renderParents = (bool) $flag;
+ return $this;
+ }
+
+ /**
+ * Returns flag indicating whether parents should be rendered when rendering only the active branch.
+ *
+ * By default, this value is true.
+ *
+ * @return bool
+ */
+ public function getRenderParents()
+ {
+ return $this->renderParents;
+ }
+
+ /**
+ * Sets CSS class to use for the first 'ul' element when rendering.
+ *
+ * @param string $ulClass CSS class to set
+ * @return self
+ */
+ public function setUlClass($ulClass)
+ {
+ if (is_string($ulClass)) {
+ $this->ulClass = $ulClass;
+ }
+ return $this;
+ }
+
+ /**
+ * Returns CSS class to use for the first 'ul' element when rendering.
+ *
+ * @return string
+ */
+ public function getUlClass()
+ {
+ return $this->ulClass;
+ }
+
+ /**
+ * Sets CSS class to use for the active 'li' element when rendering.
+ *
+ * @param string $liActiveClass CSS class to set
+ * @return self
+ */
+ public function setLiActiveClass($liActiveClass)
+ {
+ if (is_string($liActiveClass)) {
+ $this->liActiveClass = $liActiveClass;
+ }
+ return $this;
+ }
+
+ /**
+ * Returns CSS class to use for the active 'li' element when rendering.
+ *
+ * @return string
+ */
+ public function getLiActiveClass()
+ {
+ return $this->liActiveClass;
+ }
+
+ /**
+ * Render a partial with the given "model".
+ *
+ * @param null|AbstractContainer $container
+ * @param null|string|array $partial
+ * @return Partial|string
+ * @throws Exception\RuntimeException If no partial provided.
+ * @throws Exception\InvalidArgumentException If partial is invalid array.
+ */
+ protected function renderPartialModel(array $params, $container, $partial)
+ {
+ $this->parseContainer($container);
+ if (null === $container) {
+ $container = $this->getContainer();
+ }
+
+ if (null === $partial) {
+ $partial = $this->getPartial();
+ }
+
+ if (empty($partial)) {
+ throw new Exception\RuntimeException(
+ 'Unable to render menu: No partial view script provided'
+ );
+ }
+
+ $model = array_merge($params, ['container' => $container]);
+
+ /** @var Partial $partialHelper */
+ $partialHelper = $this->view->plugin('partial');
+ if (is_array($partial)) {
+ if (count($partial) !== 2) {
+ throw new Exception\InvalidArgumentException(
+ 'Unable to render menu: A view partial supplied as '
+ . 'an array must contain one value: the partial view script'
+ );
+ }
+
+ return $partialHelper($partial[0], $model);
+ }
+
+ return $partialHelper($partial, $model);
+ }
+}
diff --git a/src/View/Helper/Navigation.php b/src/View/Helper/Navigation.php
new file mode 100644
index 00000000..14f6cecc
--- /dev/null
+++ b/src/View/Helper/Navigation.php
@@ -0,0 +1,352 @@
+setContainer($container);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Magic overload: Proxy to other navigation helpers or the container
+ *
+ * Examples of usage from a view script or layout:
+ *
+ * // proxy to Menu helper and render container:
+ * echo $this->navigation()->menu();
+ *
+ * // proxy to Breadcrumbs helper and set indentation:
+ * $this->navigation()->breadcrumbs()->setIndent(8);
+ *
+ * // proxy to container and find all pages with 'blog' route:
+ * $blogPages = $this->navigation()->findAllByRoute('blog');
+ *
+ *
+ * @param string|class-string $method helper name or method name in container
+ * @param array $arguments [optional] arguments to pass
+ * @throws Exception\ExceptionInterface If proxying to a helper, and the
+ * helper is not an instance of the
+ * interface specified in
+ * {@link findHelper()}.
+ * @throws ExceptionInterface If method does not exist in container.
+ * @return mixed returns what the proxied call returns
+ */
+ public function __call($method, array $arguments = [])
+ {
+ // check if call should proxy to another helper
+ $helper = $this->findHelper($method, false);
+ if ($helper) {
+ if (method_exists($helper, 'setServiceLocator') && $this->getServiceLocator()) {
+ $helper->setServiceLocator($this->getServiceLocator());
+ }
+ return call_user_func_array($helper, $arguments);
+ }
+
+ // default behaviour: proxy call to container
+ return parent::__call($method, $arguments);
+ }
+
+ /**
+ * Renders helper
+ *
+ * @param AbstractContainer $container
+ * @return string
+ * @throws Exception\RuntimeException
+ */
+ public function render($container = null)
+ {
+ return $this->findHelper($this->getDefaultProxy())->render($container);
+ }
+
+ /**
+ * Returns the helper matching $proxy
+ *
+ * The helper must implement the interface {@link NavigationHelper}.
+ *
+ * @param string|class-string $proxy helper name
+ * @param bool $strict [optional] whether exceptions should be
+ * thrown if something goes
+ * wrong. Default is true.
+ * @throws Exception\RuntimeException If $strict is true and helper cannot be found.
+ * @return NavigationHelper|false helper instance
+ * @psalm-return ($strict is true ? NavigationHelper : NavigationHelper|false)
+ */
+ public function findHelper($proxy, $strict = true)
+ {
+ $plugins = $this->getPluginManager();
+ if (! $plugins->has($proxy)) {
+ if ($strict) {
+ throw new Exception\RuntimeException(sprintf(
+ 'Failed to find plugin for %s',
+ $proxy
+ ));
+ }
+
+ return false;
+ }
+
+ $helper = $plugins->get($proxy);
+ assert($helper instanceof NavigationHelper);
+ $container = $this->getContainer();
+ $hash = spl_object_hash($container) . spl_object_hash($helper);
+
+ if (! isset($this->injected[$hash])) {
+ $helper->setContainer();
+ $this->inject($helper);
+ $this->injected[$hash] = true;
+ } else {
+ if ($this->getInjectContainer()) {
+ $helper->setContainer($container);
+ }
+ }
+
+ return $helper;
+ }
+
+ /**
+ * Injects container, ACL, and translator to the given $helper if this
+ * helper is configured to do so
+ *
+ * @param NavigationHelper $helper helper instance
+ * @return void
+ */
+ protected function inject(NavigationHelper $helper)
+ {
+ if ($this->getInjectContainer() && ! $helper->hasContainer()) {
+ $helper->setContainer($this->getContainer());
+ }
+
+ if ($this->getInjectAcl()) {
+ if (! $helper->hasAcl()) {
+ $helper->setAcl($this->getAcl());
+ }
+ if (! $helper->hasRole()) {
+ $helper->setRole($this->getRole());
+ }
+ }
+
+ if ($this->getInjectTranslator() && ! $helper->hasTranslator()) {
+ $helper->setTranslator(
+ $this->getTranslator(),
+ $this->getTranslatorTextDomain()
+ );
+ }
+ }
+
+ /**
+ * Sets the default proxy to use in {@link render()}
+ *
+ * @param string $proxy default proxy
+ * @return Navigation
+ */
+ public function setDefaultProxy($proxy)
+ {
+ $this->defaultProxy = (string) $proxy;
+ return $this;
+ }
+
+ /**
+ * Returns the default proxy to use in {@link render()}
+ *
+ * @return string
+ */
+ public function getDefaultProxy()
+ {
+ return $this->defaultProxy;
+ }
+
+ /**
+ * Sets whether container should be injected when proxying
+ *
+ * @param bool $injectContainer
+ * @return Navigation
+ */
+ public function setInjectContainer($injectContainer = true)
+ {
+ $this->injectContainer = (bool) $injectContainer;
+ return $this;
+ }
+
+ /**
+ * Returns whether container should be injected when proxying
+ *
+ * @return bool
+ */
+ public function getInjectContainer()
+ {
+ return $this->injectContainer;
+ }
+
+ /**
+ * Sets whether ACL should be injected when proxying
+ *
+ * @param bool $injectAcl
+ * @return Navigation
+ */
+ public function setInjectAcl($injectAcl = true)
+ {
+ $this->injectAcl = (bool) $injectAcl;
+ return $this;
+ }
+
+ /**
+ * Returns whether ACL should be injected when proxying
+ *
+ * @return bool
+ */
+ public function getInjectAcl()
+ {
+ return $this->injectAcl;
+ }
+
+ /**
+ * Sets whether translator should be injected when proxying
+ *
+ * @param bool $injectTranslator
+ * @return Navigation
+ */
+ public function setInjectTranslator($injectTranslator = true)
+ {
+ $this->injectTranslator = (bool) $injectTranslator;
+ return $this;
+ }
+
+ /**
+ * Returns whether translator should be injected when proxying
+ *
+ * @return bool
+ */
+ public function getInjectTranslator()
+ {
+ return $this->injectTranslator;
+ }
+
+ /**
+ * Set manager for retrieving navigation helpers
+ *
+ * @return Navigation
+ */
+ public function setPluginManager(PluginManager $plugins)
+ {
+ $renderer = $this->getView();
+ if ($renderer) {
+ $plugins->setRenderer($renderer);
+ }
+ $this->plugins = $plugins;
+
+ return $this;
+ }
+
+ /**
+ * Retrieve plugin loader for navigation helpers
+ *
+ * Lazy-loads an instance of Navigation\HelperLoader if none currently
+ * registered.
+ *
+ * @return PluginManager
+ */
+ public function getPluginManager()
+ {
+ $pluginManager = $this->plugins;
+ if ($pluginManager === null) {
+ $pluginManager = new PluginManager($this->getServiceLocator());
+ $this->setPluginManager($pluginManager);
+ }
+
+ return $pluginManager;
+ }
+
+ /**
+ * Set the View object
+ *
+ * @return self
+ */
+ public function setView(Renderer $view)
+ {
+ parent::setView($view);
+ if ($view && $this->plugins) {
+ $this->plugins->setRenderer($view);
+ }
+ return $this;
+ }
+}
diff --git a/src/View/Helper/PluginManager.php b/src/View/Helper/PluginManager.php
new file mode 100644
index 00000000..28c34499
--- /dev/null
+++ b/src/View/Helper/PluginManager.php
@@ -0,0 +1,87 @@
+
+ */
+class PluginManager extends HelperPluginManager
+{
+ /** {@inheritDoc} */
+ protected $instanceOf = AbstractHelper::class;
+
+ /**
+ * Default aliases
+ *
+ * @var array
+ */
+ protected $aliases = [
+ 'breadcrumbs' => Breadcrumbs::class,
+ 'links' => Links::class,
+ 'menu' => Menu::class,
+ 'sitemap' => Sitemap::class,
+
+ // Legacy Zend Framework aliases
+ 'Zend\View\Helper\Navigation\Breadcrumbs' => Breadcrumbs::class,
+ 'Zend\View\Helper\Navigation\Links' => Links::class, // phpcs:ignore
+ 'Zend\View\Helper\Navigation\Menu' => Menu::class,
+ 'Zend\View\Helper\Navigation\Sitemap' => Sitemap::class,
+
+ // v2 normalized FQCNs
+ 'zendviewhelpernavigationbreadcrumbs' => Breadcrumbs::class,
+ 'zendviewhelpernavigationlinks' => Links::class,
+ 'zendviewhelpernavigationmenu' => Menu::class,
+ 'zendviewhelpernavigationsitemap' => Sitemap::class,
+ ];
+
+ /**
+ * Default factories
+ *
+ * {@inheritDoc}
+ */
+ protected $factories = [
+ Breadcrumbs::class => InvokableFactory::class,
+ Links::class => InvokableFactory::class,
+ Menu::class => InvokableFactory::class,
+ Sitemap::class => InvokableFactory::class,
+
+ // v2 canonical FQCNs
+ 'laminasviewhelpernavigationbreadcrumbs' => InvokableFactory::class,
+ 'laminasviewhelpernavigationlinks' => InvokableFactory::class,
+ 'laminasviewhelpernavigationmenu' => InvokableFactory::class,
+ 'laminasviewhelpernavigationsitemap' => InvokableFactory::class,
+ ];
+
+ /**
+ * @param ContainerInterface $configOrContainerInstance
+ * @psalm-param ServiceManagerConfiguration $v3config
+ */
+ public function __construct($configOrContainerInstance = null, array $v3config = [])
+ {
+ /** @psalm-suppress MissingClosureParamType */
+ $this->initializers[] = function (ContainerInterface $container, $instance): void {
+ if (! $instance instanceof AbstractHelper) {
+ return;
+ }
+
+ $instance->setServiceLocator($this->creationContext);
+ };
+
+ parent::__construct($configOrContainerInstance, $v3config);
+ }
+}
diff --git a/src/View/Helper/Sitemap.php b/src/View/Helper/Sitemap.php
new file mode 100644
index 00000000..aea14cc0
--- /dev/null
+++ b/src/View/Helper/Sitemap.php
@@ -0,0 +1,458 @@
+ tag
+ *
+ * @var string
+ */
+ public const SITEMAP_NS = 'http://www.sitemaps.org/schemas/sitemap/0.9';
+
+ /**
+ * Schema URL
+ *
+ * @var string
+ */
+ public const SITEMAP_XSD = 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd';
+
+ /**
+ * Whether XML output should be formatted
+ *
+ * @var bool
+ */
+ protected $formatOutput = false;
+
+ /**
+ * Server url
+ *
+ * @var string
+ */
+ protected $serverUrl;
+
+ /**
+ * List of urls in the sitemap
+ *
+ * @var array
+ */
+ protected $urls = [];
+
+ /**
+ * Whether sitemap should be validated using Laminas\Validate\Sitemap\*
+ *
+ * @var bool
+ */
+ protected $useSitemapValidators = true;
+
+ /**
+ * Whether sitemap should be schema validated when generated
+ *
+ * @var bool
+ */
+ protected $useSchemaValidation = false;
+
+ /**
+ * Whether the XML declaration should be included in XML output
+ *
+ * @var bool
+ */
+ protected $useXmlDeclaration = true;
+
+ /**
+ * Helper entry point
+ *
+ * @param string|AbstractContainer $container container to operate on
+ * @return Sitemap
+ */
+ public function __invoke($container = null)
+ {
+ if (null !== $container) {
+ $this->setContainer($container);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Renders helper
+ *
+ * Implements {@link HelperInterface::render()}.
+ *
+ * @param AbstractContainer $container [optional] container to render. Default is
+ * to render the container registered in the helper.
+ * @return string
+ */
+ public function render($container = null)
+ {
+ $dom = $this->getDomSitemap($container);
+ $xml = $this->getUseXmlDeclaration() ?
+ $dom->saveXML() :
+ $dom->saveXML($dom->documentElement);
+
+ return rtrim($xml, PHP_EOL);
+ }
+
+ /**
+ * Returns a DOMDocument containing the Sitemap XML for the given container
+ *
+ * @param AbstractContainer|null $container [optional] container to get
+ * breadcrumbs from, defaults
+ * to what is registered in the
+ * helper
+ * @return DOMDocument DOM representation of the
+ * container
+ * @throws Exception\RuntimeException If schema validation is on
+ * and the sitemap is invalid
+ * according to the sitemap
+ * schema, or if sitemap
+ * validators are used and the
+ * loc element fails validation.
+ */
+ public function getDomSitemap(?AbstractContainer $container = null)
+ {
+ // Reset the urls
+ $this->urls = [];
+
+ if (null === $container) {
+ $container = $this->getContainer();
+ }
+
+ $locValidator = null;
+ $lastmodValidator = null;
+ $changefreqValidator = null;
+ $priorityValidator = null;
+
+ // check if we should validate using our own validators
+ if ($this->getUseSitemapValidators()) {
+ // create validators
+ $locValidator = new Loc();
+ $lastmodValidator = new Lastmod();
+ $changefreqValidator = new Changefreq();
+ $priorityValidator = new Priority();
+ }
+
+ // create document
+ $dom = new DOMDocument('1.0', 'UTF-8');
+ $dom->formatOutput = $this->getFormatOutput();
+
+ // ...and urlset (root) element
+ $urlSet = $dom->createElementNS(self::SITEMAP_NS, 'urlset');
+ $dom->appendChild($urlSet);
+
+ // create iterator
+ $iterator = new RecursiveIteratorIterator($container, RecursiveIteratorIterator::SELF_FIRST);
+
+ $maxDepth = $this->getMaxDepth();
+ if (is_int($maxDepth)) {
+ $iterator->setMaxDepth($maxDepth);
+ }
+ $minDepth = $this->getMinDepth();
+ if (! is_int($minDepth) || $minDepth < 0) {
+ $minDepth = 0;
+ }
+
+ // iterate container
+ foreach ($iterator as $page) {
+ if ($iterator->getDepth() < $minDepth || ! $this->accept($page)) {
+ // page should not be included
+ continue;
+ }
+
+ // get absolute url from page
+ if (! $url = $this->url($page)) {
+ // skip page if it has no url (rare case)
+ // or already is in the sitemap
+ continue;
+ }
+
+ // create url node for this page
+ $urlNode = $dom->createElementNS(self::SITEMAP_NS, 'url');
+ $urlSet->appendChild($urlNode);
+
+ if (
+ $this->getUseSitemapValidators()
+ && ! $locValidator->isValid($url)
+ ) {
+ throw new Exception\RuntimeException(sprintf(
+ 'Encountered an invalid URL for Sitemap XML: "%s"',
+ $url
+ ));
+ }
+
+ // put url in 'loc' element
+ $urlNode->appendChild($dom->createElementNS(self::SITEMAP_NS, 'loc', $url));
+
+ // add 'lastmod' element if a valid lastmod is set in page
+ if (isset($page->lastmod)) {
+ $lastmod = strtotime((string) $page->lastmod);
+
+ // prevent 1970-01-01...
+ if ($lastmod !== false) {
+ $lastmod = date('c', $lastmod);
+ }
+
+ if (
+ ! $this->getUseSitemapValidators()
+ || $lastmodValidator->isValid($lastmod)
+ ) {
+ // Cast $lastmod to string in case no validation was used
+ $urlNode->appendChild(
+ $dom->createElementNS(self::SITEMAP_NS, 'lastmod', (string) $lastmod)
+ );
+ }
+ }
+
+ // add 'changefreq' element if a valid changefreq is set in page
+ if (isset($page->changefreq)) {
+ $changefreq = $page->changefreq;
+ if (
+ ! $this->getUseSitemapValidators() ||
+ $changefreqValidator->isValid($changefreq)
+ ) {
+ $urlNode->appendChild(
+ $dom->createElementNS(self::SITEMAP_NS, 'changefreq', $changefreq)
+ );
+ }
+ }
+
+ // add 'priority' element if a valid priority is set in page
+ if (isset($page->priority)) {
+ $priority = $page->priority;
+ if (
+ ! $this->getUseSitemapValidators() ||
+ $priorityValidator->isValid($priority)
+ ) {
+ $urlNode->appendChild(
+ $dom->createElementNS(self::SITEMAP_NS, 'priority', $priority)
+ );
+ }
+ }
+ }
+
+ // validate using schema if specified
+ if ($this->getUseSchemaValidation()) {
+ ErrorHandler::start();
+ $test = $dom->schemaValidate(self::SITEMAP_XSD);
+ $error = ErrorHandler::stop();
+ if (! $test) {
+ throw new Exception\RuntimeException(sprintf(
+ 'Sitemap is invalid according to XML Schema at "%s"',
+ self::SITEMAP_XSD
+ ), 0, $error);
+ }
+ }
+
+ return $dom;
+ }
+
+ /**
+ * Returns an escaped absolute URL for the given page
+ *
+ * @return null|string
+ */
+ public function url(AbstractPage $page)
+ {
+ $href = $page->getHref();
+
+ if (! isset($href[0])) {
+ // no href
+ return '';
+ } elseif ($href[0] === '/') {
+ // href is relative to root; use serverUrl helper
+ $url = $this->getServerUrl() . $href;
+ } elseif (preg_match('/^[a-z]+:/im', (string) $href)) {
+ // scheme is given in href; assume absolute URL already
+ $url = (string) $href;
+ } else {
+ // href is relative to current document; use url helpers
+ $basePathHelper = $this->getView()->plugin('basepath');
+ $curDoc = $basePathHelper();
+ $curDoc = '/' === $curDoc ? '' : trim($curDoc, '/');
+ $url = rtrim($this->getServerUrl(), '/') . '/'
+ . $curDoc
+ . (empty($curDoc) ? '' : '/') . $href;
+ }
+
+ if (! in_array($url, $this->urls)) {
+ $this->urls[] = $url;
+ return $this->xmlEscape($url);
+ }
+
+ return null;
+ }
+
+ /**
+ * Escapes string for XML usage
+ *
+ * @param string $string
+ * @return string
+ */
+ protected function xmlEscape($string)
+ {
+ $escaper = $this->view->plugin('escapeHtml');
+ return $escaper($string);
+ }
+
+ /**
+ * Sets whether XML output should be formatted
+ *
+ * @param bool $formatOutput
+ * @return Sitemap
+ */
+ public function setFormatOutput($formatOutput = true)
+ {
+ $this->formatOutput = (bool) $formatOutput;
+ return $this;
+ }
+
+ /**
+ * Returns whether XML output should be formatted
+ *
+ * @return bool
+ */
+ public function getFormatOutput()
+ {
+ return $this->formatOutput;
+ }
+
+ /**
+ * Sets server url (scheme and host-related stuff without request URI)
+ *
+ * E.g. http://www.example.com
+ *
+ * @param string $serverUrl
+ * @return Sitemap
+ * @throws Exception\InvalidArgumentException
+ */
+ public function setServerUrl($serverUrl)
+ {
+ $uri = Uri\UriFactory::factory($serverUrl);
+ $uri->setFragment('');
+ $uri->setPath('');
+ $uri->setQuery('');
+
+ if ($uri->isValid()) {
+ $this->serverUrl = $uri->toString();
+ } else {
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'Invalid server URL: "%s"',
+ $serverUrl
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns server URL
+ *
+ * @return string
+ */
+ public function getServerUrl()
+ {
+ if (! isset($this->serverUrl)) {
+ $serverUrlHelper = $this->getView()->plugin('serverUrl');
+ $this->serverUrl = $serverUrlHelper();
+ }
+
+ return $this->serverUrl;
+ }
+
+ /**
+ * Sets whether sitemap should be validated using Laminas\Validate\Sitemap_*
+ *
+ * @param bool $useSitemapValidators
+ * @return Sitemap
+ */
+ public function setUseSitemapValidators($useSitemapValidators)
+ {
+ $this->useSitemapValidators = (bool) $useSitemapValidators;
+ return $this;
+ }
+
+ /**
+ * Returns whether sitemap should be validated using Laminas\Validate\Sitemap_*
+ *
+ * @return bool
+ */
+ public function getUseSitemapValidators()
+ {
+ return $this->useSitemapValidators;
+ }
+
+ /**
+ * Sets whether sitemap should be schema validated when generated
+ *
+ * @param bool $schemaValidation
+ * @return Sitemap
+ */
+ public function setUseSchemaValidation($schemaValidation)
+ {
+ $this->useSchemaValidation = (bool) $schemaValidation;
+ return $this;
+ }
+
+ /**
+ * Returns true if sitemap should be schema validated when generated
+ *
+ * @return bool
+ */
+ public function getUseSchemaValidation()
+ {
+ return $this->useSchemaValidation;
+ }
+
+ /**
+ * Sets whether the XML declaration should be used in output
+ *
+ * @param bool $useXmlDecl
+ * @return Sitemap
+ */
+ public function setUseXmlDeclaration($useXmlDecl)
+ {
+ $this->useXmlDeclaration = (bool) $useXmlDecl;
+ return $this;
+ }
+
+ /**
+ * Returns whether the XML declaration should be used in output
+ *
+ * @return bool
+ */
+ public function getUseXmlDeclaration()
+ {
+ return $this->useXmlDeclaration;
+ }
+}
diff --git a/src/View/HelperConfig.php b/src/View/HelperConfig.php
index 6d903ad8..55bdc434 100644
--- a/src/View/HelperConfig.php
+++ b/src/View/HelperConfig.php
@@ -4,12 +4,12 @@
namespace Laminas\Navigation\View;
+use Laminas\Navigation\View\Helper\Navigation as NavigationHelper;
use Laminas\ServiceManager\Config;
use Laminas\ServiceManager\ConfigInterface;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Laminas\ServiceManager\ServiceManager;
use Laminas\Stdlib\ArrayUtils;
-use Laminas\View\Helper\Navigation as NavigationHelper;
use Psr\Container\ContainerInterface;
use ReflectionProperty;
use Traversable;
diff --git a/src/View/NavigationHelperFactory.php b/src/View/NavigationHelperFactory.php
index 81e4f5b4..587bddae 100644
--- a/src/View/NavigationHelperFactory.php
+++ b/src/View/NavigationHelperFactory.php
@@ -4,9 +4,9 @@
namespace Laminas\Navigation\View;
+use Laminas\Navigation\View\Helper\Navigation as NavigationHelper;
use Laminas\ServiceManager\FactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
-use Laminas\View\Helper\Navigation as NavigationHelper;
use Psr\Container\ContainerInterface;
use ReflectionProperty;
diff --git a/test/ServiceFactoryTest.php b/test/ServiceFactoryTest.php
index 7adc9412..f3dc6cca 100644
--- a/test/ServiceFactoryTest.php
+++ b/test/ServiceFactoryTest.php
@@ -128,7 +128,7 @@ public function testConstructedNavigationFactoryInjectRouterAndMatcher(): void
{
$builder = $this->getMockBuilder(ConstructedNavigationFactory::class);
$builder->setConstructorArgs([__DIR__ . '/_files/navigation_mvc.xml'])
- ->setMethods(['injectComponents']);
+ ->onlyMethods(['injectComponents']);
$factory = $builder->getMock();
diff --git a/test/TestAsset/ArrayTranslator.php b/test/TestAsset/ArrayTranslator.php
new file mode 100644
index 00000000..979b87a7
--- /dev/null
+++ b/test/TestAsset/ArrayTranslator.php
@@ -0,0 +1,23 @@
+translations);
+ }
+}
diff --git a/test/View/Helper/AbstractHelperTest.php b/test/View/Helper/AbstractHelperTest.php
new file mode 100644
index 00000000..6f70d48c
--- /dev/null
+++ b/test/View/Helper/AbstractHelperTest.php
@@ -0,0 +1,97 @@
+helper = new NavigationHelper\Breadcrumbs();
+ parent::setUp();
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ $this->helper->setDefaultAcl(null);
+ $this->helper->setAcl(null);
+ $this->helper->setDefaultRole(null);
+ $this->helper->setRole(null);
+ }
+
+ public function testHasACLChecksDefaultACL(): void
+ {
+ $aclContainer = $this->getAcl();
+ $acl = $aclContainer['acl'];
+
+ $this->assertEquals(false, $this->helper->hasACL());
+ $this->helper->setDefaultAcl($acl);
+ $this->assertEquals(true, $this->helper->hasAcl());
+ }
+
+ public function testHasACLChecksMemberVariable(): void
+ {
+ $aclContainer = $this->getAcl();
+ $acl = $aclContainer['acl'];
+
+ $this->assertEquals(false, $this->helper->hasAcl());
+ $this->helper->setAcl($acl);
+ $this->assertEquals(true, $this->helper->hasAcl());
+ }
+
+ public function testHasRoleChecksDefaultRole(): void
+ {
+ $aclContainer = $this->getAcl();
+ $role = $aclContainer['role'];
+
+ $this->assertEquals(false, $this->helper->hasRole());
+ $this->helper->setDefaultRole($role);
+ $this->assertEquals(true, $this->helper->hasRole());
+ }
+
+ public function testHasRoleChecksMemberVariable(): void
+ {
+ $aclContainer = $this->getAcl();
+ $role = $aclContainer['role'];
+
+ $this->assertEquals(false, $this->helper->hasRole());
+ $this->helper->setRole($role);
+ $this->assertEquals(true, $this->helper->hasRole());
+ }
+
+ public function testEventManagerIsNullByDefault(): void
+ {
+ $this->assertNull($this->helper->getEventManager());
+ }
+
+ public function testFallBackForContainerNames(): void
+ {
+ // Register navigation service with name equal to the documentation
+ $this->serviceManager->setAllowOverride(true);
+ $this->serviceManager->setService(
+ 'navigation',
+ $this->serviceManager->get('Navigation')
+ );
+ $this->serviceManager->setAllowOverride(false);
+
+ $this->helper->setServiceLocator($this->serviceManager);
+
+ $this->helper->setContainer('navigation');
+ $this->assertInstanceOf(
+ Navigation::class,
+ $this->helper->getContainer()
+ );
+
+ $this->helper->setContainer('default');
+ $this->assertInstanceOf(
+ Navigation::class,
+ $this->helper->getContainer()
+ );
+ }
+}
diff --git a/test/View/Helper/AbstractTestCase.php b/test/View/Helper/AbstractTestCase.php
new file mode 100644
index 00000000..dbb1bd05
--- /dev/null
+++ b/test/View/Helper/AbstractTestCase.php
@@ -0,0 +1,210 @@
+nav1 = new Navigation($config['nav_test1']);
+ $this->nav2 = new Navigation($config['nav_test2']);
+ $this->nav3 = new Navigation($config['nav_test3']);
+
+ // setup view
+ $view = new PhpRenderer();
+ $resolver = $view->resolver();
+ assert($resolver instanceof TemplatePathStack);
+ $resolver->addPath(__DIR__ . '/_files/mvc/views');
+
+ // inject view into test subject helper
+ $this->helper->setView($view);
+
+ // set nav1 in helper as default
+ $this->helper->setContainer($this->nav1);
+
+ // setup service manager
+ $smConfig = [
+ 'modules' => [],
+ 'module_listener_options' => [
+ 'config_cache_enabled' => false,
+ 'cache_dir' => 'data/cache',
+ 'module_paths' => [],
+ 'extra_config' => [
+ 'service_manager' => [
+ 'factories' => [
+ 'config' => static fn(): array => [
+ 'navigation' => [
+ 'default' => $config['nav_test1'],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ $sm = $this->serviceManager = new ServiceManager();
+ $sm->setAllowOverride(true);
+
+ (new ServiceManagerConfig())->configureServiceManager($sm);
+
+ $routerConfig = new Config((new RouterConfigProvider())->getDependencyConfig());
+ $routerConfig->configureServiceManager($sm);
+
+ $sm->setService('ApplicationConfig', $smConfig);
+ $moduleManager = $sm->get('ModuleManager');
+ self::assertInstanceOf(ModuleManager::class, $moduleManager);
+ $moduleManager->loadModules();
+ $application = $sm->get('Application');
+ self::assertInstanceOf(Application::class, $application);
+ $application->bootstrap();
+ $sm->setFactory('Navigation', DefaultNavigationFactory::class);
+
+ $sm->setService('nav1', $this->nav1);
+ $sm->setService('nav2', $this->nav2);
+
+ $sm->setAllowOverride(false);
+ $application->getMvcEvent()->setRouteMatch(new RouteMatch([
+ 'controller' => 'post',
+ 'action' => 'view',
+ 'id' => '1337',
+ ]));
+ }
+
+ /**
+ * Returns the expected contents of the given $filename
+ */
+ protected function getExpectedFileContents(string $filename): string
+ {
+ return file_get_contents(__DIR__ . '/_files/expected/' . $filename);
+ }
+
+ /**
+ * @return array{acl: Acl, role: 'special'}
+ */
+ protected function getAcl(): array
+ {
+ $acl = new Acl();
+
+ $acl->addRole(new GenericRole('guest'));
+ $acl->addRole(new GenericRole('member'), 'guest');
+ $acl->addRole(new GenericRole('admin'), 'member');
+ $acl->addRole(new GenericRole('special'), 'member');
+
+ $acl->addResource(new GenericResource('guest_foo'));
+ $acl->addResource(new GenericResource('member_foo'), 'guest_foo');
+ $acl->addResource(new GenericResource('admin_foo'));
+ $acl->addResource(new GenericResource('special_foo'), 'member_foo');
+
+ $acl->allow('guest', 'guest_foo');
+ $acl->allow('member', 'member_foo');
+ $acl->allow('admin', 'admin_foo');
+ $acl->allow('special', 'special_foo');
+ $acl->allow('special', 'admin_foo', 'read');
+
+ return ['acl' => $acl, 'role' => 'special'];
+ }
+
+ protected function getTranslator(): Translator
+ {
+ $loader = new ArrayTranslator();
+ $loader->translations = [
+ 'Page 1' => 'Side 1',
+ 'Page 1.1' => 'Side 1.1',
+ 'Page 2' => 'Side 2',
+ 'Page 2.3' => 'Side 2.3',
+ 'Page 2.3.3.1' => 'Side 2.3.3.1',
+ 'Home' => 'Hjem',
+ 'Go home' => 'GÃ¥ hjem',
+ ];
+ $translator = new Translator();
+ $translator->getPluginManager()->setService('default', $loader);
+ $translator->addTranslationFile('default', __FILE__);
+ return $translator;
+ }
+
+ protected function getTranslatorWithTextDomain(): Translator
+ {
+ $loader1 = new ArrayTranslator();
+ $loader1->translations = [
+ 'Page 1' => 'TextDomain1 1',
+ 'Page 1.1' => 'TextDomain1 1.1',
+ 'Page 2' => 'TextDomain1 2',
+ 'Page 2.3' => 'TextDomain1 2.3',
+ 'Page 2.3.3' => 'TextDomain1 2.3.3',
+ 'Page 2.3.3.1' => 'TextDomain1 2.3.3.1',
+ ];
+
+ $loader2 = new ArrayTranslator();
+ $loader2->translations = [
+ 'Page 1' => 'TextDomain2 1',
+ 'Page 1.1' => 'TextDomain2 1.1',
+ 'Page 2' => 'TextDomain2 2',
+ 'Page 2.3' => 'TextDomain2 2.3',
+ 'Page 2.3.3' => 'TextDomain2 2.3.3',
+ 'Page 2.3.3.1' => 'TextDomain2 2.3.3.1',
+ ];
+
+ $translator = new Translator();
+ $translator->getPluginManager()->setService('default1', $loader1);
+ $translator->getPluginManager()->setService('default2', $loader2);
+ $translator->addTranslationFile('default1', __FILE__, 'LaminasTest_1');
+ $translator->addTranslationFile('default2', __FILE__, 'LaminasTest_2');
+ return $translator;
+ }
+}
diff --git a/test/View/Helper/BreadcrumbsTest.php b/test/View/Helper/BreadcrumbsTest.php
new file mode 100644
index 00000000..c12a4d52
--- /dev/null
+++ b/test/View/Helper/BreadcrumbsTest.php
@@ -0,0 +1,250 @@
+helper = new Breadcrumbs();
+ parent::setUp();
+ }
+
+ public function testCanRenderStraightFromServiceAlias(): void
+ {
+ $this->helper->setServiceLocator($this->serviceManager);
+
+ $returned = $this->helper->renderStraight('Navigation');
+ $this->assertEquals($returned, $this->getExpectedFileContents('bc/default.html'));
+ }
+
+ public function testCanRenderPartialFromServiceAlias(): void
+ {
+ $this->helper->setPartial('bc.phtml');
+ $this->helper->setServiceLocator($this->serviceManager);
+
+ $returned = $this->helper->renderPartial('Navigation');
+ $this->assertEquals($returned, $this->getExpectedFileContents('bc/partial.html'));
+ }
+
+ public function testHelperEntryPointWithoutAnyParams(): void
+ {
+ $returned = $this->helper->__invoke();
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav1, $returned->getContainer());
+ }
+
+ public function testHelperEntryPointWithContainerParam(): void
+ {
+ $returned = $this->helper->__invoke($this->nav2);
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav2, $returned->getContainer());
+ }
+
+ public function testHelperEntryPointWithContainerStringParam(): void
+ {
+ $this->helper->setServiceLocator($this->serviceManager);
+
+ $returned = $this->helper->__invoke('nav1');
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav1, $returned->getContainer());
+ }
+
+ public function testNullOutContainer(): void
+ {
+ $old = $this->helper->getContainer();
+ $this->helper->setContainer();
+ $new = $this->helper->getContainer();
+
+ $this->assertNotEquals($old, $new);
+ }
+
+ public function testSetSeparator(): void
+ {
+ $this->helper->setSeparator('foo');
+
+ $expected = $this->getExpectedFileContents('bc/separator.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testSetMaxDepth(): void
+ {
+ $this->helper->setMaxDepth(1);
+
+ $expected = $this->getExpectedFileContents('bc/maxdepth.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testSetMinDepth(): void
+ {
+ $this->helper->setMinDepth(1);
+
+ $expected = '';
+ $this->assertEquals($expected, $this->helper->render($this->nav2));
+ }
+
+ public function testLinkLastElement(): void
+ {
+ $this->helper->setLinkLast(true);
+
+ $expected = $this->getExpectedFileContents('bc/linklast.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testSetIndent(): void
+ {
+ $this->helper->setIndent(8);
+
+ $expected = ' helper->render(), 0, strlen($expected));
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testRenderSuppliedContainerWithoutInterfering(): void
+ {
+ $this->helper->setMinDepth(0);
+
+ $rendered1 = $this->getExpectedFileContents('bc/default.html');
+ $rendered2 = 'Site 2 ';
+
+ $expected = [
+ 'registered' => $rendered1,
+ 'supplied' => $rendered2,
+ 'registered_again' => $rendered1,
+ ];
+
+ $actual = [
+ 'registered' => $this->helper->render(),
+ 'supplied' => $this->helper->render($this->nav2),
+ 'registered_again' => $this->helper->render(),
+ ];
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testUseAclResourceFromPages(): void
+ {
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole($acl['role']);
+
+ $expected = $this->getExpectedFileContents('bc/acl.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testTranslationUsingLaminasTranslate(): void
+ {
+ if (! extension_loaded('intl')) {
+ $this->markTestSkipped('ext/intl not enabled');
+ }
+
+ $this->helper->setTranslator($this->getTranslator());
+
+ $expected = $this->getExpectedFileContents('bc/translated.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testTranslationUsingLaminasTranslateAndCustomTextDomain(): void
+ {
+ if (! extension_loaded('intl')) {
+ $this->markTestSkipped('ext/intl not enabled');
+ }
+
+ $this->helper->setTranslator($this->getTranslatorWithTextDomain());
+
+ $expected = $this->getExpectedFileContents('bc/textdomain.html');
+ $test = $this->helper->render($this->nav3);
+
+ $this->assertEquals(trim($expected), trim($test));
+ }
+
+ public function testTranslationUsingLaminasTranslateAdapter(): void
+ {
+ if (! extension_loaded('intl')) {
+ $this->markTestSkipped('ext/intl not enabled');
+ }
+
+ $translator = $this->getTranslator();
+ $this->helper->setTranslator($translator);
+
+ $expected = $this->getExpectedFileContents('bc/translated.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testDisablingTranslation(): void
+ {
+ $translator = $this->getTranslator();
+ $this->helper->setTranslator($translator);
+ $this->helper->setTranslatorEnabled(false);
+
+ $expected = $this->getExpectedFileContents('bc/default.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testRenderingPartial(): void
+ {
+ $this->helper->setPartial('bc.phtml');
+
+ $expected = $this->getExpectedFileContents('bc/partial.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testRenderingPartialWithSeparator(): void
+ {
+ $this->helper->setPartial('bc_separator.phtml')->setSeparator(' / ');
+
+ $expected = trim($this->getExpectedFileContents('bc/partialwithseparator.html'));
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testRenderingPartialBySpecifyingAnArrayAsPartial(): void
+ {
+ $this->helper->setPartial(['bc.phtml', 'application']);
+
+ $expected = $this->getExpectedFileContents('bc/partial.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testRenderingPartialShouldFailOnInvalidPartialArray(): void
+ {
+ $this->helper->setPartial(['bc.phtml']);
+ $this->expectException(InvalidArgumentException::class);
+ $this->helper->render();
+ }
+
+ public function testRenderingPartialWithParams(): void
+ {
+ $this->helper->setPartial('bc_with_partial_params.phtml')->setSeparator(' / ');
+ $expected = $this->getExpectedFileContents('bc/partial_with_params.html');
+ $actual = $this->helper->renderPartialWithParams(['variable' => 'test value']);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testLastBreadcrumbShouldBeEscaped(): void
+ {
+ $container = new Navigation([
+ [
+ 'label' => 'Live & Learn',
+ 'uri' => '#',
+ 'active' => true,
+ ],
+ ]);
+
+ $expected = 'Live & Learn ';
+ $actual = $this->helper->setMinDepth(0)->render($container);
+
+ $this->assertEquals($expected, $actual);
+ }
+}
diff --git a/test/View/Helper/LinksTest.php b/test/View/Helper/LinksTest.php
new file mode 100644
index 00000000..ae8b876d
--- /dev/null
+++ b/test/View/Helper/LinksTest.php
@@ -0,0 +1,726 @@
+helper = new Links();
+ parent::setUp();
+
+ // doctype fix (someone forgot to clean up after their unit tests)
+ $renderer = $this->helper->getView();
+ assert($renderer instanceof PhpRenderer);
+ $helper = $renderer->plugin(Doctype::class);
+ $this->doctypeHelper = $helper;
+ $this->doctypeHelper->setDoctype(
+ Doctype::HTML4_LOOSE
+ );
+
+ // disable all active pages
+ foreach ($this->helper->findAllByActive(true) as $page) {
+ $page->active = false;
+ }
+ }
+
+ public function testCanRenderFromServiceAlias(): void
+ {
+ $this->helper->setServiceLocator($this->serviceManager);
+
+ $returned = $this->helper->render('Navigation');
+ $this->assertEquals($returned, $this->getExpectedFileContents('links/default.html'));
+ }
+
+ public function testHelperEntryPointWithoutAnyParams(): void
+ {
+ $returned = $this->helper->__invoke();
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav1, $returned->getContainer());
+ }
+
+ public function testHelperEntryPointWithContainerParam(): void
+ {
+ $returned = $this->helper->__invoke($this->nav2);
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav2, $returned->getContainer());
+ }
+
+ public function testDoNotRenderIfNoPageIsActive(): void
+ {
+ $this->assertEquals('', $this->helper->render());
+ }
+
+ public function testDetectRelationFromStringPropertyOfActivePage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+ $active->addRel('example', 'http://www.example.com/');
+ $found = $this->helper->findRelation($active, 'rel', 'example');
+
+ $expected = [
+ 'type' => UriPage::class,
+ 'href' => 'http://www.example.com/',
+ 'label' => null,
+ ];
+
+ $actual = [
+ 'type' => $found !== null ? $found::class : self::class,
+ 'href' => $found->getHref(),
+ 'label' => $found->getLabel(),
+ ];
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDetectRelationFromPageInstancePropertyOfActivePage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+ $active->addRel('example', AbstractPage::factory([
+ 'uri' => 'http://www.example.com/',
+ 'label' => 'An example page',
+ ]));
+ $found = $this->helper->findRelExample($active);
+
+ $expected = [
+ 'type' => UriPage::class,
+ 'href' => 'http://www.example.com/',
+ 'label' => 'An example page',
+ ];
+
+ $actual = [
+ 'type' => $found::class,
+ 'href' => $found->getHref(),
+ 'label' => $found->getLabel(),
+ ];
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDetectRelationFromArrayPropertyOfActivePage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+ $active->addRel('example', [
+ 'uri' => 'http://www.example.com/',
+ 'label' => 'An example page',
+ ]);
+ $found = $this->helper->findRelExample($active);
+
+ $expected = [
+ 'type' => UriPage::class,
+ 'href' => 'http://www.example.com/',
+ 'label' => 'An example page',
+ ];
+
+ $actual = [
+ 'type' => $found::class,
+ 'href' => $found->getHref(),
+ 'label' => $found->getLabel(),
+ ];
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDetectRelationFromArrayObjectInstancePropertyOfActivePage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+ $active->addRel('example', new ArrayObject([
+ 'uri' => 'http://www.example.com/',
+ 'label' => 'An example page',
+ ]));
+ $found = $this->helper->findRelExample($active);
+
+ $expected = [
+ 'type' => UriPage::class,
+ 'href' => 'http://www.example.com/',
+ 'label' => 'An example page',
+ ];
+
+ $actual = [
+ 'type' => $found::class,
+ 'href' => $found->getHref(),
+ 'label' => $found->getLabel(),
+ ];
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDetectMultipleRelationsFromArrayPropertyOfActivePage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+
+ $active->addRel('alternate', [
+ [
+ 'label' => 'foo',
+ 'uri' => 'bar',
+ ],
+ [
+ 'label' => 'baz',
+ 'uri' => 'bat',
+ ],
+ ]);
+
+ $found = $this->helper->findRelAlternate($active);
+
+ $expected = ['type' => 'array', 'count' => 2];
+ $actual = ['type' => gettype($found), 'count' => count($found)];
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDetectMultipleRelationsFromArrayObjectPropertyOfActivePage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+
+ $active->addRel('alternate', new ArrayObject([
+ [
+ 'label' => 'foo',
+ 'uri' => 'bar',
+ ],
+ [
+ 'label' => 'baz',
+ 'uri' => 'bat',
+ ],
+ ]));
+
+ $found = $this->helper->findRelAlternate($active);
+
+ $expected = ['type' => 'array', 'count' => 2];
+ $actual = ['type' => gettype($found), 'count' => count($found)];
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testExtractingRelationsFromPageProperties(): void
+ {
+ $types = [
+ 'alternate',
+ 'stylesheet',
+ 'start',
+ 'next',
+ 'prev',
+ 'contents',
+ 'index',
+ 'glossary',
+ 'copyright',
+ 'chapter',
+ 'section',
+ 'subsection',
+ 'appendix',
+ 'help',
+ 'bookmark',
+ ];
+
+ $samplePage = AbstractPage::factory([
+ 'label' => 'An example page',
+ 'uri' => 'http://www.example.com/',
+ ]);
+
+ $active = $this->helper->findOneByLabel('Page 2');
+ $expected = [];
+ $actual = [];
+
+ foreach ($types as $type) {
+ $active->addRel($type, $samplePage);
+ $found = $this->helper->findRelation($active, 'rel', $type);
+
+ $expected[$type] = $samplePage->getLabel();
+ $actual[$type] = $found->getLabel();
+
+ $active->removeRel($type);
+ }
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindStartPageByTraversal(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.1');
+ $expected = 'Home';
+ $actual = $this->helper->findRelStart($active)->getLabel();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDoNotFindStartWhenGivenPageIsTheFirstPage(): void
+ {
+ $active = $this->helper->findOneByLabel('Home');
+ $actual = $this->helper->findRelStart($active);
+ $this->assertNull($actual, 'Should not find any start page');
+ }
+
+ public function testFindNextPageByTraversalShouldFindChildPage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+ $expected = 'Page 2.1';
+ $actual = $this->helper->findRelNext($active)->getLabel();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindNextPageByTraversalShouldFindSiblingPage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.1');
+ $expected = 'Page 2.2';
+ $actual = $this->helper->findRelNext($active)->getLabel();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindNextPageByTraversalShouldWrap(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.2.2');
+ $expected = 'Page 2.3';
+ $actual = $this->helper->findRelNext($active)->getLabel();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindPrevPageByTraversalShouldFindParentPage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.1');
+ $expected = 'Page 2';
+ $actual = $this->helper->findRelPrev($active)->getLabel();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindPrevPageByTraversalShouldFindSiblingPage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.2');
+ $expected = 'Page 2.1';
+ $actual = $this->helper->findRelPrev($active)->getLabel();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindPrevPageByTraversalShouldWrap(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.3');
+ $expected = 'Page 2.2.2';
+ $actual = $this->helper->findRelPrev($active)->getLabel();
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testShouldFindChaptersFromFirstLevelOfPagesInContainer(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.3');
+ $found = $this->helper->findRelChapter($active);
+
+ $expected = ['Page 1', 'Page 2', 'Page 3', 'Zym'];
+ $actual = [];
+ foreach ($found as $page) {
+ $actual[] = $page->getLabel();
+ }
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindingChaptersShouldExcludeSelfIfChapter(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+ $found = $this->helper->findRelChapter($active);
+
+ $expected = ['Page 1', 'Page 3', 'Zym'];
+ $actual = [];
+ foreach ($found as $page) {
+ $actual[] = $page->getLabel();
+ }
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindSectionsWhenActiveChapterPage(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+ $found = $this->helper->findRelSection($active);
+ $expected = ['Page 2.1', 'Page 2.2', 'Page 2.3'];
+ $actual = [];
+ foreach ($found as $page) {
+ $actual[] = $page->getLabel();
+ }
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDoNotFindSectionsWhenActivePageIsASection(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.2');
+ $found = $this->helper->findRelSection($active);
+ $this->assertNull($found);
+ }
+
+ public function testDoNotFindSectionsWhenActivePageIsASubsection(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.2.1');
+ $found = $this->helper->findRelation($active, 'rel', 'section');
+ $this->assertNull($found);
+ }
+
+ public function testFindSubsectionWhenActivePageIsSection(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.2');
+ $found = $this->helper->findRelSubsection($active);
+
+ $expected = ['Page 2.2.1', 'Page 2.2.2'];
+ $actual = [];
+ foreach ($found as $page) {
+ $actual[] = $page->getLabel();
+ }
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDoNotFindSubsectionsWhenActivePageIsASubSubsection(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.2.1');
+ $found = $this->helper->findRelSubsection($active);
+ $this->assertNull($found);
+ }
+
+ public function testDoNotFindSubsectionsWhenActivePageIsAChapter(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2');
+ $found = $this->helper->findRelSubsection($active);
+ $this->assertNull($found);
+ }
+
+ public function testFindRevSectionWhenPageIsSection(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.2');
+ $found = $this->helper->findRevSection($active);
+ $this->assertEquals('Page 2', $found->getLabel());
+ }
+
+ public function testFindRevSubsectionWhenPageIsSubsection(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 2.2.1');
+ $found = $this->helper->findRevSubsection($active);
+ $this->assertEquals('Page 2.2', $found->getLabel());
+ }
+
+ public function testAclFiltersAwayPagesFromPageProperty(): void
+ {
+ $acl = new Acl\Acl();
+ $acl->addRole(new Role\GenericRole('member'));
+ $acl->addRole(new Role\GenericRole('admin'));
+ $acl->addResource(new Resource\GenericResource('protected'));
+ $acl->allow('admin', 'protected');
+ $this->helper->setAcl($acl);
+ $this->helper->setRole($acl->getRole('member'));
+
+ $samplePage = AbstractPage::factory([
+ 'label' => 'An example page',
+ 'uri' => 'http://www.example.com/',
+ 'resource' => 'protected',
+ ]);
+
+ $active = $this->helper->findOneByLabel('Home');
+ $expected = [
+ 'alternate' => false,
+ 'stylesheet' => false,
+ 'start' => false,
+ 'next' => 'Page 1',
+ 'prev' => false,
+ 'contents' => false,
+ 'index' => false,
+ 'glossary' => false,
+ 'copyright' => false,
+ 'chapter' => 'array(4)',
+ 'section' => false,
+ 'subsection' => false,
+ 'appendix' => false,
+ 'help' => false,
+ 'bookmark' => false,
+ ];
+ $actual = [];
+
+ foreach ($expected as $type => $discard) {
+ $active->addRel($type, $samplePage);
+
+ $found = $this->helper->findRelation($active, 'rel', $type);
+ if (null === $found) {
+ $actual[$type] = false;
+ } elseif (is_array($found)) {
+ $actual[$type] = 'array(' . count($found) . ')';
+ } else {
+ $actual[$type] = $found->getLabel();
+ }
+ }
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testAclFiltersAwayPagesFromContainerSearch(): void
+ {
+ $acl = new Acl\Acl();
+ $acl->addRole(new Role\GenericRole('member'));
+ $acl->addRole(new Role\GenericRole('admin'));
+ $acl->addResource(new Resource\GenericResource('protected'));
+ $acl->allow('admin', 'protected');
+ $this->helper->setAcl($acl);
+ $this->helper->setRole($acl->getRole('member'));
+
+ $this->helper->getContainer();
+ $container = $this->helper->getContainer();
+ $iterator = new RecursiveIteratorIterator(
+ $container,
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+ foreach ($iterator as $page) {
+ $page->resource = 'protected';
+ }
+ $this->helper->setContainer($container);
+
+ $this->helper->findOneByLabel('Home');
+ $search = [
+ 'start' => 'Page 1',
+ 'next' => 'Page 1',
+ 'prev' => 'Page 1.1',
+ 'chapter' => 'Home',
+ 'section' => 'Page 1',
+ 'subsection' => 'Page 2.2',
+ ];
+
+ $expected = [];
+ $actual = [];
+
+ foreach ($search as $type => $active) {
+ $expected[$type] = false;
+
+ $active = $this->helper->findOneByLabel($active);
+ $found = $this->helper->findRelation($active, 'rel', $type);
+
+ if (null === $found) {
+ $actual[$type] = false;
+ } elseif (is_array($found)) {
+ $actual[$type] = 'array(' . count($found) . ')';
+ } else {
+ $actual[$type] = $found->getLabel();
+ }
+ }
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testFindRelationMustSpecifyRelOrRev(): void
+ {
+ $active = $this->helper->findOneByLabel('Home');
+ try {
+ $this->helper->findRelation($active, 'foo', 'bar');
+ $this->fail('An invalid value was given, but a '
+ . 'Laminas\View\Exception\InvalidArgumentException was not thrown');
+ } catch (View\Exception\ExceptionInterface $e) {
+ $this->assertStringContainsString('Invalid argument: $rel', $e->getMessage());
+ }
+ }
+
+ public function testRenderLinkMustSpecifyRelOrRev(): void
+ {
+ $active = $this->helper->findOneByLabel('Home');
+ try {
+ $this->helper->renderLink($active, 'foo', 'bar');
+ $this->fail('An invalid value was given, but a '
+ . 'Laminas\View\Exception\InvalidArgumentException was not thrown');
+ } catch (View\Exception\ExceptionInterface $e) {
+ $this->assertStringContainsString('Invalid relation attribute', $e->getMessage());
+ }
+ }
+
+ public function testFindAllRelations(): void
+ {
+ $expectedRelations = [
+ 'alternate' => ['Forced page'],
+ 'stylesheet' => ['Forced page'],
+ 'start' => ['Forced page'],
+ 'next' => ['Forced page'],
+ 'prev' => ['Forced page'],
+ 'contents' => ['Forced page'],
+ 'index' => ['Forced page'],
+ 'glossary' => ['Forced page'],
+ 'copyright' => ['Forced page'],
+ 'chapter' => ['Forced page'],
+ 'section' => ['Forced page'],
+ 'subsection' => ['Forced page'],
+ 'appendix' => ['Forced page'],
+ 'help' => ['Forced page'],
+ 'bookmark' => ['Forced page'],
+ 'canonical' => ['Forced page'],
+ 'home' => ['Forced page'],
+ ];
+
+ // build expected result
+ $expected = [
+ 'rel' => $expectedRelations,
+ 'rev' => $expectedRelations,
+ ];
+
+ // find active page and create page to use for relations
+ $active = $this->helper->findOneByLabel('Page 1');
+ $forcedRelation = new UriPage([
+ 'label' => 'Forced page',
+ 'uri' => '#',
+ ]);
+
+ // add relations to active page
+ foreach ($expectedRelations as $type => $discard) {
+ $active->addRel($type, $forcedRelation);
+ $active->addRev($type, $forcedRelation);
+ }
+
+ // build actual result
+ $actual = $this->helper->findAllRelations($active);
+ foreach ($actual as $attrib => $relations) {
+ foreach ($relations as $type => $pages) {
+ foreach ($pages as $key => $page) {
+ $actual[$attrib][$type][$key] = $page->getLabel();
+ }
+ }
+ }
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ // @codingStandardsIgnoreStart
+ /**
+ * @return string[]
+ *
+ * @psalm-return array{1: 'alternate', 2: 'stylesheet', 4: 'start', 8: 'next', 16: 'prev', 32: 'contents', 64: 'index', 128: 'glossary', 512: 'chapter', 1024: 'section', 2048: 'subsection', 4096: 'appendix', 8192: 'help', 16384: 'bookmark', 32768: 'canonical'}
+ */
+ private function _getFlags(): array
+ {
+ // @codingStandardsIgnoreEnd
+ return [
+ Links::RENDER_ALTERNATE => 'alternate',
+ Links::RENDER_STYLESHEET => 'stylesheet',
+ Links::RENDER_START => 'start',
+ Links::RENDER_NEXT => 'next',
+ Links::RENDER_PREV => 'prev',
+ Links::RENDER_CONTENTS => 'contents',
+ Links::RENDER_INDEX => 'index',
+ Links::RENDER_GLOSSARY => 'glossary',
+ Links::RENDER_CHAPTER => 'chapter',
+ Links::RENDER_SECTION => 'section',
+ Links::RENDER_SUBSECTION => 'subsection',
+ Links::RENDER_APPENDIX => 'appendix',
+ Links::RENDER_HELP => 'help',
+ Links::RENDER_BOOKMARK => 'bookmark',
+ Links::RENDER_CUSTOM => 'canonical',
+ ];
+ }
+
+ public function testSingleRenderFlags(): void
+ {
+ $active = $this->helper->findOneByLabel('Home');
+ $active->active = true;
+
+ $expected = [];
+ $actual = [];
+
+ // build expected and actual result
+ foreach ($this->_getFlags() as $newFlag => $type) {
+ // add forced relation
+ $active->addRel($type, 'http://www.example.com/');
+ $active->addRev($type, 'http://www.example.com/');
+
+ $this->helper->setRenderFlag($newFlag);
+ $expectedOutput = ' ' . PHP_EOL
+ . ' ';
+ $actualOutput = $this->helper->render();
+
+ $expected[$type] = $expectedOutput;
+ $actual[$type] = $actualOutput;
+
+ // remove forced relation
+ $active->removeRel($type);
+ $active->removeRev($type);
+ }
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testRenderFlagBitwiseOr(): void
+ {
+ $newFlag = Links::RENDER_NEXT |
+ Links::RENDER_PREV;
+ $this->helper->setRenderFlag($newFlag);
+ $active = $this->helper->findOneByLabel('Page 1.1');
+ $active->active = true;
+
+ // test data
+ $expected = ' '
+ . PHP_EOL
+ . ' ';
+ $actual = $this->helper->render();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testIndenting(): void
+ {
+ $active = $this->helper->findOneByLabel('Page 1.1');
+ $newFlag = Links::RENDER_NEXT |
+ Links::RENDER_PREV;
+ $this->helper->setRenderFlag($newFlag);
+ $this->helper->setIndent(' ');
+ $active->active = true;
+
+ // build expected and actual result
+ $expected = ' '
+ . PHP_EOL
+ . ' ';
+ $actual = $this->helper->render();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testSetMaxDepth(): void
+ {
+ $this->helper->setMaxDepth(1);
+ $this->helper->findOneByLabel('Page 2.3.3')->setActive(); // level 2
+ $flag = Links::RENDER_NEXT;
+
+ $expected = ' ';
+ $actual = $this->helper->setRenderFlag($flag)->render();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testSetMinDepth(): void
+ {
+ $this->helper->setMinDepth(2);
+ $this->helper->findOneByLabel('Page 2.3')->setActive(); // level 1
+ $flag = Links::RENDER_NEXT;
+
+ $expected = '';
+ $actual = $this->helper->setRenderFlag($flag)->render();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ /** @inheritDoc */
+ protected function getExpectedFileContents(string $filename): string
+ {
+ return str_replace("\n", PHP_EOL, parent::getExpectedFileContents($filename));
+ }
+}
diff --git a/test/View/Helper/MenuTest.php b/test/View/Helper/MenuTest.php
new file mode 100644
index 00000000..e6610754
--- /dev/null
+++ b/test/View/Helper/MenuTest.php
@@ -0,0 +1,609 @@
+helper = new Menu();
+ parent::setUp();
+ }
+
+ public function testCanRenderMenuFromServiceAlias(): void
+ {
+ $this->helper->setServiceLocator($this->serviceManager);
+
+ $returned = $this->helper->renderMenu('Navigation');
+ $this->assertEquals($returned, $this->getExpectedFileContents('menu/default1.html'));
+ }
+
+ public function testCanRenderPartialFromServiceAlias(): void
+ {
+ $this->helper->setPartial('menu.phtml');
+ $this->helper->setServiceLocator($this->serviceManager);
+
+ $returned = $this->helper->renderPartial('Navigation');
+ $this->assertEquals($returned, $this->getExpectedFileContents('menu/partial.html'));
+ }
+
+ public function testHelperEntryPointWithoutAnyParams(): void
+ {
+ $returned = $this->helper->__invoke();
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav1, $returned->getContainer());
+ }
+
+ public function testHelperEntryPointWithContainerParam(): void
+ {
+ $returned = $this->helper->__invoke($this->nav2);
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav2, $returned->getContainer());
+ }
+
+ public function testNullingOutContainerInHelper(): void
+ {
+ $this->helper->setContainer();
+ $this->assertEquals(0, count($this->helper->getContainer()));
+ }
+
+ public function testSetIndentAndOverrideInRenderMenu(): void
+ {
+ $this->helper->setIndent(8);
+
+ $expected = [
+ 'indent4' => $this->getExpectedFileContents('menu/indent4.html'),
+ 'indent8' => $this->getExpectedFileContents('menu/indent8.html'),
+ ];
+
+ $renderOptions = [
+ 'indent' => 4,
+ ];
+
+ $actual = [
+ 'indent4' => rtrim($this->helper->renderMenu(null, $renderOptions), PHP_EOL),
+ 'indent8' => rtrim($this->helper->renderMenu(), PHP_EOL),
+ ];
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testRenderSuppliedContainerWithoutInterfering(): void
+ {
+ $rendered1 = $this->getExpectedFileContents('menu/default1.html');
+ $rendered2 = $this->getExpectedFileContents('menu/default2.html');
+ $expected = [
+ 'registered' => $rendered1,
+ 'supplied' => $rendered2,
+ 'registered_again' => $rendered1,
+ ];
+
+ $actual = [
+ 'registered' => $this->helper->render(),
+ 'supplied' => $this->helper->render($this->nav2),
+ 'registered_again' => $this->helper->render(),
+ ];
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testUseAclRoleAsString(): void
+ {
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole('member');
+
+ $expected = $this->getExpectedFileContents('menu/acl_string.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testFilterOutPagesBasedOnAcl(): void
+ {
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole($acl['role']);
+
+ $expected = $this->getExpectedFileContents('menu/acl.html');
+ $actual = $this->helper->render();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testDisablingAcl(): void
+ {
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole($acl['role']);
+ $this->helper->setUseAcl(false);
+
+ $expected = $this->getExpectedFileContents('menu/default1.html');
+ $actual = $this->helper->render();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testUseAnAclRoleInstanceFromAclObject(): void
+ {
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole($acl['acl']->getRole('member'));
+
+ $expected = $this->getExpectedFileContents('menu/acl_role_interface.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testUseConstructedAclRolesNotFromAclObject(): void
+ {
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole(new GenericRole('member'));
+
+ $expected = $this->getExpectedFileContents('menu/acl_role_interface.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testSetUlCssClass(): void
+ {
+ $this->helper->setUlClass('My_Nav');
+ $expected = $this->getExpectedFileContents('menu/css.html');
+ $this->assertEquals($expected, $this->helper->render($this->nav2));
+ }
+
+ public function testSetLiActiveCssClass(): void
+ {
+ $this->helper->setLiActiveClass('activated');
+ $expected = $this->getExpectedFileContents('menu/css2.html');
+ $this->assertEquals(trim($expected), $this->helper->render($this->nav2));
+ }
+
+ public function testOptionEscapeLabelsAsTrue(): void
+ {
+ $options = [
+ 'escapeLabels' => true,
+ ];
+
+ $container = new Navigation($this->nav2->toArray());
+ $container->addPage([
+ 'label' => 'Badges 1 ',
+ 'uri' => 'badges',
+ ]);
+
+ $expected = $this->getExpectedFileContents('menu/escapelabels_as_true.html');
+ $actual = $this->helper->renderMenu($container, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionEscapeLabelsAsFalse(): void
+ {
+ $options = [
+ 'escapeLabels' => false,
+ ];
+
+ $container = new Navigation($this->nav2->toArray());
+ $container->addPage([
+ 'label' => 'Badges 1 ',
+ 'uri' => 'badges',
+ ]);
+
+ $expected = $this->getExpectedFileContents('menu/escapelabels_as_false.html');
+ $actual = $this->helper->renderMenu($container, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testTranslationUsingLaminasTranslate(): void
+ {
+ if (! extension_loaded('intl')) {
+ $this->markTestSkipped('ext/intl not enabled');
+ }
+
+ $translator = $this->getTranslator();
+ $this->helper->setTranslator($translator);
+
+ $expected = $this->getExpectedFileContents('menu/translated.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testTranslationUsingLaminasTranslateWithTextDomain(): void
+ {
+ if (! extension_loaded('intl')) {
+ $this->markTestSkipped('ext/intl not enabled');
+ }
+
+ $translator = $this->getTranslatorWithTextDomain();
+ $this->helper->setTranslator($translator);
+
+ $expected = $this->getExpectedFileContents('menu/textdomain.html');
+ $test = $this->helper->render($this->nav3);
+ $this->assertEquals(trim($expected), trim($test));
+ }
+
+ public function testTranslationUsingLaminasTranslateAdapter(): void
+ {
+ if (! extension_loaded('intl')) {
+ $this->markTestSkipped('ext/intl not enabled');
+ }
+
+ $translator = $this->getTranslator();
+ $this->helper->setTranslator($translator);
+
+ $expected = $this->getExpectedFileContents('menu/translated.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testDisablingTranslation(): void
+ {
+ $translator = $this->getTranslator();
+ $this->helper->setTranslator($translator);
+ $this->helper->setTranslatorEnabled(false);
+
+ $expected = $this->getExpectedFileContents('menu/default1.html');
+ $this->assertEquals($expected, $this->helper->render());
+ }
+
+ public function testRenderingPartial(): void
+ {
+ $this->helper->setPartial('menu.phtml');
+
+ $expected = $this->getExpectedFileContents('menu/partial.html');
+ $actual = $this->helper->render();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testRenderingPartialBySpecifyingAnArrayAsPartial(): void
+ {
+ $this->helper->setPartial(['menu.phtml', 'application']);
+
+ $expected = $this->getExpectedFileContents('menu/partial.html');
+ $actual = $this->helper->render();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testRenderingPartialWithParams(): void
+ {
+ $this->helper->setPartial(['menu_with_partial_params.phtml', 'application']);
+ $expected = $this->getExpectedFileContents('menu/partial_with_params.html');
+ $actual = $this->helper->renderPartialWithParams(['variable' => 'test value']);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testRenderingPartialShouldFailOnInvalidPartialArray(): void
+ {
+ $this->helper->setPartial(['menu.phtml']);
+ $this->expectException(InvalidArgumentException::class);
+ $this->helper->render();
+ }
+
+ public function testSetMaxDepth(): void
+ {
+ $this->helper->setMaxDepth(1);
+
+ $expected = $this->getExpectedFileContents('menu/maxdepth.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testSetMinDepth(): void
+ {
+ $this->helper->setMinDepth(1);
+
+ $expected = $this->getExpectedFileContents('menu/mindepth.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testSetBothDepts(): void
+ {
+ $this->helper->setMinDepth(1)->setMaxDepth(2);
+
+ $expected = $this->getExpectedFileContents('menu/bothdepts.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testSetOnlyActiveBranch(): void
+ {
+ $this->helper->setOnlyActiveBranch(true);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testSetRenderParents(): void
+ {
+ $this->helper->setOnlyActiveBranch(true)->setRenderParents(false);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_noparents.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testSetOnlyActiveBranchAndMinDepth(): void
+ {
+ $this->helper->setOnlyActiveBranch()->setMinDepth(1);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_mindepth.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOnlyActiveBranchAndMaxDepth(): void
+ {
+ $this->helper->setOnlyActiveBranch()->setMaxDepth(2);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_maxdepth.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOnlyActiveBranchAndBothDepthsSpecified(): void
+ {
+ $this->helper->setOnlyActiveBranch()->setMinDepth(1)->setMaxDepth(2);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_bothdepts.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOnlyActiveBranchNoParentsAndBothDepthsSpecified(): void
+ {
+ $this->helper->setOnlyActiveBranch()
+ ->setMinDepth(1)
+ ->setMaxDepth(2)
+ ->setRenderParents(false);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_np_bd.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ // @codingStandardsIgnoreStart
+ private function _setActive(string $label): void
+ {
+ // @codingStandardsIgnoreEnd
+ $container = $this->helper->getContainer();
+
+ foreach ($container->findAllByActive(true) as $page) {
+ $page->setActive(false);
+ }
+
+ if ($p = $container->findOneByLabel($label)) {
+ $p->setActive(true);
+ }
+ }
+
+ public function testOnlyActiveBranchNoParentsActiveOneBelowMinDepth(): void
+ {
+ $this->_setActive('Page 2');
+
+ $this->helper->setOnlyActiveBranch()
+ ->setMinDepth(1)
+ ->setMaxDepth(1)
+ ->setRenderParents(false);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_np_bd2.html');
+ $actual = $this->helper->renderMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testRenderSubMenuShouldOverrideOptions(): void
+ {
+ $this->helper->setOnlyActiveBranch(false)
+ ->setMinDepth(1)
+ ->setMaxDepth(2)
+ ->setRenderParents(true);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_noparents.html');
+ $actual = $this->helper->renderSubMenu();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionMaxDepth(): void
+ {
+ $options = [
+ 'maxDepth' => 1,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/maxdepth.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionMinDepth(): void
+ {
+ $options = [
+ 'minDepth' => 1,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/mindepth.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionBothDepts(): void
+ {
+ $options = [
+ 'minDepth' => 1,
+ 'maxDepth' => 2,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/bothdepts.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionOnlyActiveBranch(): void
+ {
+ $options = [
+ 'onlyActiveBranch' => true,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionOnlyActiveBranchNoParents(): void
+ {
+ $options = [
+ 'onlyActiveBranch' => true,
+ 'renderParents' => false,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_noparents.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionOnlyActiveBranchAndMinDepth(): void
+ {
+ $options = [
+ 'minDepth' => 1,
+ 'onlyActiveBranch' => true,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_mindepth.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionOnlyActiveBranchAndMaxDepth(): void
+ {
+ $options = [
+ 'maxDepth' => 2,
+ 'onlyActiveBranch' => true,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_maxdepth.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionOnlyActiveBranchAndBothDepthsSpecified(): void
+ {
+ $options = [
+ 'minDepth' => 1,
+ 'maxDepth' => 2,
+ 'onlyActiveBranch' => true,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_bothdepts.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testOptionOnlyActiveBranchNoParentsAndBothDepthsSpecified(): void
+ {
+ $options = [
+ 'minDepth' => 2,
+ 'maxDepth' => 2,
+ 'onlyActiveBranch' => true,
+ 'renderParents' => false,
+ ];
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_np_bd.html');
+ $actual = $this->helper->renderMenu(null, $options);
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testRenderingWithoutPageClassToLi(): void
+ {
+ $container = new Navigation($this->nav2->toArray());
+ $container->addPage([
+ 'label' => 'Class test',
+ 'uri' => 'test',
+ 'class' => 'foobar',
+ ]);
+
+ $expected = $this->getExpectedFileContents('menu/addclasstolistitem_as_false.html');
+ $actual = $this->helper->renderMenu($container);
+
+ $this->assertEquals(trim($expected), trim($actual));
+ }
+
+ public function testRenderingWithPageClassToLi(): void
+ {
+ $options = [
+ 'addClassToListItem' => true,
+ ];
+
+ $container = new Navigation($this->nav2->toArray());
+ $container->addPage([
+ 'label' => 'Class test',
+ 'uri' => 'test',
+ 'class' => 'foobar',
+ ]);
+
+ $expected = $this->getExpectedFileContents('menu/addclasstolistitem_as_true.html');
+ $actual = $this->helper->renderMenu($container, $options);
+
+ $this->assertEquals(trim($expected), trim($actual));
+ }
+
+ public function testRenderDeepestMenuWithPageClassToLi(): void
+ {
+ $options = [
+ 'addClassToListItem' => true,
+ 'onlyActiveBranch' => true,
+ 'renderParents' => false,
+ ];
+
+ /** @var array[] $pages */
+ $pages = $this->nav2->toArray();
+ $pages[1]['class'] = 'foobar';
+ $container = new Navigation($pages);
+
+ $expected = $this->getExpectedFileContents('menu/onlyactivebranch_addclasstolistitem.html');
+ $actual = $this->helper->renderMenu($container, $options);
+
+ $this->assertEquals(trim($expected), trim($actual));
+ }
+
+ /** @inheritDoc */
+ protected function getExpectedFileContents(string $filename): string
+ {
+ return str_replace("\n", PHP_EOL, parent::getExpectedFileContents($filename));
+ }
+}
diff --git a/test/View/Helper/NavigationTest.php b/test/View/Helper/NavigationTest.php
new file mode 100644
index 00000000..b7881c0d
--- /dev/null
+++ b/test/View/Helper/NavigationTest.php
@@ -0,0 +1,586 @@
+helper = new Navigation();
+ parent::setUp();
+ }
+
+ public function testHelperEntryPointWithoutAnyParams(): void
+ {
+ $returned = $this->helper->__invoke();
+ self::assertEquals($this->helper, $returned);
+ self::assertEquals($this->nav1, $returned->getContainer());
+ }
+
+ public function testHelperEntryPointWithContainerParam(): void
+ {
+ $returned = $this->helper->__invoke($this->nav2);
+ self::assertEquals($this->helper, $returned);
+ self::assertEquals($this->nav2, $returned->getContainer());
+ }
+
+ public function testAcceptAclShouldReturnGracefullyWithUnknownResource(): void
+ {
+ // setup
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole($acl['role']);
+
+ $accepted = $this->helper->accept(
+ new Page\Uri([
+ 'resource' => 'unknownresource',
+ 'privilege' => 'someprivilege',
+ ], false)
+ );
+
+ self::assertEquals($accepted, false);
+ }
+
+ public function testShouldProxyToMenuHelperByDefault(): void
+ {
+ $this->helper->setContainer($this->nav1);
+ $this->helper->setServiceLocator(new ServiceManager());
+
+ // result
+ $expected = $this->getExpectedFileContents('menu/default1.html');
+ $actual = $this->helper->render();
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testHasContainer(): void
+ {
+ $oldContainer = $this->helper->getContainer();
+ $this->helper->setContainer(null);
+ self::assertFalse($this->helper->hasContainer());
+ $this->helper->setContainer($oldContainer);
+ }
+
+ public function testInjectingContainer(): void
+ {
+ // setup
+ $this->helper->setContainer($this->nav2);
+ $this->helper->setServiceLocator(new ServiceManager());
+ $expected = [
+ 'menu' => $this->getExpectedFileContents('menu/default2.html'),
+ 'breadcrumbs' => $this->getExpectedFileContents('bc/default.html'),
+ ];
+ $actual = [];
+
+ // result
+ $actual['menu'] = $this->helper->render();
+ $this->helper->setContainer($this->nav1);
+ $actual['breadcrumbs'] = $this->helper->breadcrumbs()->render();
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testDisablingContainerInjection(): void
+ {
+ // setup
+ $this->helper->setServiceLocator(new ServiceManager());
+ $this->helper->setInjectContainer(false);
+ $this->helper->menu()->setContainer(null);
+ $this->helper->breadcrumbs()->setContainer(null);
+ $this->helper->setContainer($this->nav2);
+
+ // result
+ $expected = [
+ 'menu' => '',
+ 'breadcrumbs' => '',
+ ];
+ $actual = [
+ 'menu' => $this->helper->render(),
+ 'breadcrumbs' => $this->helper->breadcrumbs()->render(),
+ ];
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testMultipleNavigationsAndOneMenuDisplayedTwoTimes(): void
+ {
+ $this->helper->setServiceLocator(new ServiceManager());
+ $expected = $this->helper->setContainer($this->nav1)->menu()->getContainer();
+ $this->helper->setContainer($this->nav2)->menu()->getContainer();
+ $actual = $this->helper->setContainer($this->nav1)->menu()->getContainer();
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testServiceManagerIsUsedToRetrieveContainer(): void
+ {
+ $container = new Container();
+ $serviceManager = new ServiceManager();
+ $serviceManager->setService('navigation', $container);
+
+ new View\HelperPluginManager($serviceManager);
+
+ $this->helper->setServiceLocator($serviceManager);
+ $this->helper->setContainer('navigation');
+
+ $expected = $this->helper->getContainer();
+ $actual = $container;
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testInjectingAcl(): void
+ {
+ // setup
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole($acl['role']);
+ $this->helper->setServiceLocator(new ServiceManager());
+
+ $expected = $this->getExpectedFileContents('menu/acl.html');
+ $actual = $this->helper->render();
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testDisablingAclInjection(): void
+ {
+ // setup
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole($acl['role']);
+ $this->helper->setInjectAcl(false);
+ $this->helper->setServiceLocator(new ServiceManager());
+
+ $expected = $this->getExpectedFileContents('menu/default1.html');
+ $actual = $this->helper->render();
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testInjectingTranslator(): void
+ {
+ if (! extension_loaded('intl')) {
+ self::markTestSkipped('ext/intl not enabled');
+ }
+
+ $this->helper->setTranslator($this->getTranslator());
+ $this->helper->setServiceLocator(new ServiceManager());
+
+ $expected = $this->getExpectedFileContents('menu/translated.html');
+ $actual = $this->helper->render();
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testDisablingTranslatorInjection(): void
+ {
+ $this->helper->setTranslator($this->getTranslator());
+ $this->helper->setInjectTranslator(false);
+ $this->helper->setServiceLocator(new ServiceManager());
+
+ $expected = $this->getExpectedFileContents('menu/default1.html');
+ $actual = $this->helper->render();
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testTranslatorMethods(): void
+ {
+ $translatorMock = $this->createMock(Translator::class);
+ $this->helper->setTranslator($translatorMock, 'foo');
+
+ self::assertEquals($translatorMock, $this->helper->getTranslator());
+ self::assertEquals('foo', $this->helper->getTranslatorTextDomain());
+ self::assertTrue($this->helper->hasTranslator());
+ self::assertTrue($this->helper->isTranslatorEnabled());
+
+ $this->helper->setTranslatorEnabled(false);
+ self::assertFalse($this->helper->isTranslatorEnabled());
+ }
+
+ public function testSpecifyingDefaultProxy(): void
+ {
+ $expected = [
+ 'breadcrumbs' => $this->getExpectedFileContents('bc/default.html'),
+ 'menu' => $this->getExpectedFileContents('menu/default1.html'),
+ ];
+ $actual = [];
+
+ // result
+ $this->helper->setServiceLocator(new ServiceManager());
+ $this->helper->setDefaultProxy('breadcrumbs');
+ $actual['breadcrumbs'] = $this->helper->render($this->nav1);
+ $this->helper->setDefaultProxy('menu');
+ $actual['menu'] = $this->helper->render($this->nav1);
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testGetAclReturnsNullIfNoAclInstance(): void
+ {
+ self::assertNull($this->helper->getAcl());
+ }
+
+ public function testGetAclReturnsAclInstanceSetWithSetAcl(): void
+ {
+ $acl = new Acl\Acl();
+ $this->helper->setAcl($acl);
+ self::assertEquals($acl, $this->helper->getAcl());
+ }
+
+ public function testGetAclReturnsAclInstanceSetWithSetDefaultAcl(): void
+ {
+ $acl = new Acl\Acl();
+ AbstractHelper::setDefaultAcl($acl);
+ $actual = $this->helper->getAcl();
+ AbstractHelper::setDefaultAcl(null);
+ self::assertEquals($acl, $actual);
+ }
+
+ public function testSetDefaultAclAcceptsNull(): void
+ {
+ $acl = new Acl\Acl();
+ AbstractHelper::setDefaultAcl($acl);
+ AbstractHelper::setDefaultAcl(null);
+ self::assertNull($this->helper->getAcl());
+ }
+
+ public function testSetDefaultAclAcceptsNoParam(): void
+ {
+ $acl = new Acl\Acl();
+ AbstractHelper::setDefaultAcl($acl);
+ AbstractHelper::setDefaultAcl();
+ self::assertNull($this->helper->getAcl());
+ }
+
+ public function testSetRoleAcceptsString(): void
+ {
+ $this->helper->setRole('member');
+ self::assertEquals('member', $this->helper->getRole());
+ }
+
+ public function testSetRoleAcceptsRoleInterface(): void
+ {
+ $role = new Role\GenericRole('member');
+ $this->helper->setRole($role);
+ self::assertEquals($role, $this->helper->getRole());
+ }
+
+ public function testSetRoleAcceptsNull(): void
+ {
+ $this->helper->setRole('member')->setRole(null);
+ self::assertNull($this->helper->getRole());
+ }
+
+ public function testSetRoleAcceptsNoParam(): void
+ {
+ $this->helper->setRole('member')->setRole();
+ self::assertNull($this->helper->getRole());
+ }
+
+ public function testSetRoleThrowsExceptionWhenGivenAnInt(): void
+ {
+ try {
+ $this->helper->setRole(1337);
+ self::fail('An invalid argument was given, but a '
+ . 'Laminas\View\Exception\InvalidArgumentException was not thrown');
+ } catch (View\Exception\ExceptionInterface $e) {
+ self::assertStringContainsString('$role must be a string', $e->getMessage());
+ }
+ }
+
+ public function testSetRoleThrowsExceptionWhenGivenAnArbitraryObject(): void
+ {
+ try {
+ $this->helper->setRole(new stdClass());
+ self::fail('An invalid argument was given, but a '
+ . 'Laminas\View\Exception\InvalidArgumentException was not thrown');
+ } catch (View\Exception\ExceptionInterface $e) {
+ self::assertStringContainsString('$role must be a string', $e->getMessage());
+ }
+ }
+
+ public function testSetDefaultRoleAcceptsString(): void
+ {
+ $expected = 'member';
+ AbstractHelper::setDefaultRole($expected);
+ $actual = $this->helper->getRole();
+ AbstractHelper::setDefaultRole(null);
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testSetDefaultRoleAcceptsRoleInterface(): void
+ {
+ $expected = new Role\GenericRole('member');
+ AbstractHelper::setDefaultRole($expected);
+ $actual = $this->helper->getRole();
+ AbstractHelper::setDefaultRole(null);
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testSetDefaultRoleAcceptsNull(): void
+ {
+ AbstractHelper::setDefaultRole(null);
+ self::assertNull($this->helper->getRole());
+ }
+
+ public function testSetDefaultRoleAcceptsNoParam(): void
+ {
+ AbstractHelper::setDefaultRole();
+ self::assertNull($this->helper->getRole());
+ }
+
+ public function testSetDefaultRoleThrowsExceptionWhenGivenAnInt(): void
+ {
+ try {
+ AbstractHelper::setDefaultRole(1337);
+ self::fail('An invalid argument was given, but a '
+ . 'Laminas\View\Exception\InvalidArgumentException was not thrown');
+ } catch (View\Exception\ExceptionInterface $e) {
+ self::assertStringContainsString('$role must be', $e->getMessage());
+ }
+ }
+
+ public function testSetDefaultRoleThrowsExceptionWhenGivenAnArbitraryObject(): void
+ {
+ try {
+ AbstractHelper::setDefaultRole(new stdClass());
+ self::fail('An invalid argument was given, but a '
+ . 'Laminas\View\Exception\InvalidArgumentException was not thrown');
+ } catch (View\Exception\ExceptionInterface $e) {
+ self::assertStringContainsString('$role must be', $e->getMessage());
+ }
+ }
+
+ public function testMagicToStringShouldNotThrowException(): void
+ {
+ set_error_handler(function (int $code, string $message) {
+ $this->errorHandlerMessage = $message;
+ });
+
+ $this->helper->menu()->setPartial([1337]);
+ $this->helper->__toString();
+ restore_error_handler();
+
+ self::assertStringContainsString('array must contain', $this->errorHandlerMessage);
+ }
+
+ public function testPageIdShouldBeNormalized(): void
+ {
+ $nl = PHP_EOL;
+
+ $container = new Container([
+ [
+ 'label' => 'Page 1',
+ 'id' => 'p1',
+ 'uri' => 'p1',
+ ],
+ [
+ 'label' => 'Page 2',
+ 'id' => 'p2',
+ 'uri' => 'p2',
+ ],
+ ]);
+
+ $expected = '' . $nl
+ . ' ' . $nl
+ . ' ' . $nl
+ . ' ' . $nl
+ . ' ' . $nl
+ . ' ' . $nl
+ . ' ' . $nl
+ . ' ';
+
+ $this->helper->setServiceLocator(new ServiceManager());
+ $actual = $this->helper->render($container);
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testRenderInvisibleItem(): void
+ {
+ $container = new Container([
+ [
+ 'label' => 'Page 1',
+ 'id' => 'p1',
+ 'uri' => 'p1',
+ ],
+ [
+ 'label' => 'Page 2',
+ 'id' => 'p2',
+ 'uri' => 'p2',
+ 'visible' => false,
+ ],
+ ]);
+
+ $this->helper->setServiceLocator(new ServiceManager());
+ $render = $this->helper->menu()->render($container);
+
+ self::assertStringNotContainsString('p2', $render);
+
+ $this->helper->menu()->setRenderInvisible();
+
+ $render = $this->helper->menu()->render($container);
+
+ self::assertStringContainsString('p2', $render);
+ }
+
+ public function testMultipleNavigations(): void
+ {
+ $sm = new ServiceManager();
+ $nav1 = new Container();
+ $nav2 = new Container();
+ $sm->setService('nav1', $nav1);
+ $sm->setService('nav2', $nav2);
+
+ $helper = new Navigation();
+ $helper->setServiceLocator($sm);
+
+ $menu = $helper('nav1')->menu();
+ $actual = spl_object_hash($nav1);
+ $expected = spl_object_hash($menu->getContainer());
+ self::assertEquals($expected, $actual);
+
+ $menu = $helper('nav2')->menu();
+ $actual = spl_object_hash($nav2);
+ $expected = spl_object_hash($menu->getContainer());
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testMultipleNavigationsWithDifferentHelpersAndDifferentContainers(): void
+ {
+ $sm = new ServiceManager();
+ $nav1 = new Container();
+ $nav2 = new Container();
+ $sm->setService('nav1', $nav1);
+ $sm->setService('nav2', $nav2);
+
+ $helper = new Navigation();
+ $helper->setServiceLocator($sm);
+
+ $menu = $helper('nav1')->menu();
+ $actual = spl_object_hash($nav1);
+ $expected = spl_object_hash($menu->getContainer());
+ self::assertEquals($expected, $actual);
+
+ $breadcrumbs = $helper('nav2')->breadcrumbs();
+ $actual = spl_object_hash($nav2);
+ $expected = spl_object_hash($breadcrumbs->getContainer());
+ self::assertEquals($expected, $actual);
+
+ $links = $helper()->links();
+ $expected = spl_object_hash($links->getContainer());
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testMultipleNavigationsWithDifferentHelpersAndSameContainer(): void
+ {
+ $sm = new ServiceManager();
+ $nav1 = new Container();
+ $sm->setService('nav1', $nav1);
+
+ $helper = new Navigation();
+ $helper->setServiceLocator($sm);
+
+ // Tests
+ $menu = $helper('nav1')->menu();
+ $actual = spl_object_hash($nav1);
+ $expected = spl_object_hash($menu->getContainer());
+ self::assertEquals($expected, $actual);
+
+ $breadcrumbs = $helper('nav1')->breadcrumbs();
+ $expected = spl_object_hash($breadcrumbs->getContainer());
+ self::assertEquals($expected, $actual);
+
+ $links = $helper()->links();
+ $expected = spl_object_hash($links->getContainer());
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testMultipleNavigationsWithSameHelperAndSameContainer(): void
+ {
+ $sm = new ServiceManager();
+ $nav1 = new Container();
+ $sm->setService('nav1', $nav1);
+
+ $helper = new Navigation();
+ $helper->setServiceLocator($sm);
+
+ // Test
+ $menu = $helper('nav1')->menu();
+ $actual = spl_object_hash($nav1);
+ $expected = spl_object_hash($menu->getContainer());
+ self::assertEquals($expected, $actual);
+
+ $menu = $helper('nav1')->menu();
+ $expected = spl_object_hash($menu->getContainer());
+ self::assertEquals($expected, $actual);
+
+ $menu = $helper()->menu();
+ $expected = spl_object_hash($menu->getContainer());
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testSetPluginManagerAndView(): void
+ {
+ $pluginManager = new PluginManager(new ServiceManager());
+ $view = new PhpRenderer();
+
+ $helper = new Navigation();
+ $helper->setPluginManager($pluginManager);
+ $helper->setView($view);
+
+ self::assertEquals($view, $pluginManager->getRenderer());
+ }
+
+ public function testInjectsLazyInstantiatedPluginManagerWithCurrentServiceLocator(): void
+ {
+ $services = $this->createMock(ContainerInterface::class);
+ $helper = new Navigation();
+ $helper->setServiceLocator($services);
+
+ $plugins = $helper->getPluginManager();
+ self::assertInstanceOf(PluginManager::class, $plugins);
+
+ $pluginsReflection = new ReflectionObject($plugins);
+ $creationContext = $pluginsReflection->getProperty('creationContext');
+ $creationContextValue = $creationContext->getValue($plugins);
+ self::assertSame($creationContextValue, $services);
+ }
+
+ /** @inheritDoc */
+ protected function getExpectedFileContents(string $filename): string
+ {
+ return str_replace("\n", PHP_EOL, parent::getExpectedFileContents($filename));
+ }
+}
diff --git a/test/View/Helper/PluginManagerCompatibilityTest.php b/test/View/Helper/PluginManagerCompatibilityTest.php
new file mode 100644
index 00000000..a5b66257
--- /dev/null
+++ b/test/View/Helper/PluginManagerCompatibilityTest.php
@@ -0,0 +1,51 @@
+ [
+ 'default' => [],
+ ],
+ ]);
+
+ $services = new ServiceManager();
+ $config->configureServiceManager($services);
+ $helpers = new PluginManager($services);
+
+ $helper = $helpers->get('breadcrumbs');
+ $this->assertInstanceOf(Breadcrumbs::class, $helper);
+ $this->assertSame($services, $helper->getServiceLocator());
+ }
+}
diff --git a/test/View/Helper/SitemapTest.php b/test/View/Helper/SitemapTest.php
new file mode 100644
index 00000000..ea0d453d
--- /dev/null
+++ b/test/View/Helper/SitemapTest.php
@@ -0,0 +1,280 @@
+ */
+ private array $oldServer = [];
+ /**
+ * Stores the original set timezone
+ *
+ * @var non-empty-string
+ */
+ private string $originaltimezone;
+
+ protected function setUp(): void
+ {
+ $this->helper = new Sitemap();
+ $this->originaltimezone = date_default_timezone_get();
+ date_default_timezone_set('Europe/Berlin');
+
+ if (isset($_SERVER['SERVER_NAME'])) {
+ $this->oldServer['SERVER_NAME'] = $_SERVER['SERVER_NAME'];
+ }
+
+ if (isset($_SERVER['SERVER_PORT'])) {
+ $this->oldServer['SERVER_PORT'] = $_SERVER['SERVER_PORT'];
+ }
+
+ if (isset($_SERVER['REQUEST_URI'])) {
+ $this->oldServer['REQUEST_URI'] = $_SERVER['REQUEST_URI'];
+ }
+
+ $_SERVER['SERVER_NAME'] = 'localhost';
+ $_SERVER['SERVER_PORT'] = 80;
+ $_SERVER['REQUEST_URI'] = '/';
+
+ parent::setUp();
+
+ $this->helper->setFormatOutput(true);
+ $basePath = $this->helper->getView()->plugin(BasePath::class);
+ self::assertInstanceOf(BasePath::class, $basePath);
+ $basePath->setBasePath('');
+ }
+
+ protected function tearDown(): void
+ {
+ foreach ($this->oldServer as $key => $value) {
+ $_SERVER[$key] = $value;
+ }
+ date_default_timezone_set($this->originaltimezone);
+ }
+
+ public function testHelperEntryPointWithoutAnyParams(): void
+ {
+ $returned = $this->helper->__invoke();
+ self::assertInstanceOf(Sitemap::class, $returned);
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav1, $returned->getContainer());
+ }
+
+ public function testHelperEntryPointWithContainerParam(): void
+ {
+ $returned = $this->helper->__invoke($this->nav2);
+ self::assertInstanceOf(Sitemap::class, $returned);
+ $this->assertEquals($this->helper, $returned);
+ $this->assertEquals($this->nav2, $returned->getContainer());
+ }
+
+ public function testNullingOutNavigation(): void
+ {
+ $this->helper->setContainer();
+ $this->assertEquals(0, count($this->helper->getContainer()));
+ }
+
+ public function testRenderSuppliedContainerWithoutInterfering(): void
+ {
+ $rendered1 = trim($this->getExpectedFileContents('sitemap/default1.xml'));
+ $rendered2 = trim($this->getExpectedFileContents('sitemap/default2.xml'));
+
+ $expected = [
+ 'registered' => $rendered1,
+ 'supplied' => $rendered2,
+ 'registered_again' => $rendered1,
+ ];
+ $actual = [
+ 'registered' => $this->helper->render(),
+ 'supplied' => $this->helper->render($this->nav2),
+ 'registered_again' => $this->helper->render(),
+ ];
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testUseAclRoles(): void
+ {
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole($acl['role']);
+
+ $expected = $this->getExpectedFileContents('sitemap/acl.xml');
+ $this->assertEquals(trim($expected), $this->helper->render());
+ }
+
+ public function testUseAclButNoRole(): void
+ {
+ $acl = $this->getAcl();
+ $this->helper->setAcl($acl['acl']);
+ $this->helper->setRole(null);
+
+ $expected = $this->getExpectedFileContents('sitemap/acl2.xml');
+ $this->assertEquals(trim($expected), $this->helper->render());
+ }
+
+ public function testSettingMaxDepth(): void
+ {
+ $this->helper->setMaxDepth(0);
+
+ $expected = $this->getExpectedFileContents('sitemap/depth1.xml');
+ $this->assertEquals(trim($expected), $this->helper->render());
+ }
+
+ public function testSettingMinDepth(): void
+ {
+ $this->helper->setMinDepth(1);
+
+ $expected = $this->getExpectedFileContents('sitemap/depth2.xml');
+ $this->assertEquals(trim($expected), $this->helper->render());
+ }
+
+ public function testSettingBothDepths(): void
+ {
+ $this->helper->setMinDepth(1)->setMaxDepth(2);
+
+ $expected = $this->getExpectedFileContents('sitemap/depth3.xml');
+ $this->assertEquals(trim($expected), $this->helper->render());
+ }
+
+ public function testDropXmlDeclaration(): void
+ {
+ $this->helper->setUseXmlDeclaration(false);
+
+ $expected = $this->getExpectedFileContents('sitemap/nodecl.xml');
+ $this->assertEquals(trim($expected), $this->helper->render($this->nav2));
+ }
+
+ /**
+ * @return never
+ * @psalm-suppress UnevaluatedCode
+ */
+ public function testThrowExceptionOnInvalidLoc()
+ {
+ $this->markTestIncomplete('Laminas\URI changes affect this test');
+ $nav = clone $this->nav2;
+ $nav->addPage(['label' => 'Invalid', 'uri' => 'http://w.']);
+
+ try {
+ $this->helper->render($nav);
+ } catch (View\Exception\ExceptionInterface $e) {
+ $expected = sprintf(
+ 'Encountered an invalid URL for Sitemap XML: "%s"',
+ 'http://w.'
+ );
+ $actual = $e->getMessage();
+ static::assertEquals($expected, $actual);
+ return;
+ }
+
+ static::fail('A Laminas\View\Exception\InvalidArgumentException was not thrown on invalid ');
+ }
+
+ public function testDisablingValidators(): void
+ {
+ $nav = clone $this->nav2;
+ $nav->addPage(['label' => 'Invalid', 'uri' => 'http://w.']);
+ $this->helper->setUseSitemapValidators(false);
+
+ $expected = $this->getExpectedFileContents('sitemap/invalid.xml');
+ self::assertNotEmpty($expected);
+
+ // using DOMDocument::saveXML() to prevent differences in libxml from invalidating test
+ $expectedDom = new DOMDocument();
+ $receivedDom = new DOMDocument();
+ $expectedDom->loadXML($expected);
+ $rendered = $this->helper->render($nav);
+ self::assertNotEmpty($rendered);
+ $receivedDom->loadXML($rendered);
+ $this->assertEquals($expectedDom->saveXML(), $receivedDom->saveXML());
+ }
+
+ /** @return array, 2:string}> */
+ public static function invalidServerUrlDataProvider(): array
+ {
+ return [
+ 'muppets' => [
+ 'muppets',
+ View\Exception\InvalidArgumentException::class,
+ 'Invalid server URL: "muppets"',
+ ],
+ ];
+ }
+
+ /** @param class-string $expectedType */
+ #[DataProvider('invalidServerUrlDataProvider')]
+ public function testSetServerUrlRequiresValidUri(
+ string $invalidServerUrl,
+ string $expectedType,
+ string $expectedMessage
+ ): void {
+ $this->expectException($expectedType);
+ $this->expectExceptionMessage($expectedMessage);
+ $this->helper->setServerUrl($invalidServerUrl);
+ }
+
+ public function testSetServerUrlWithSchemeAndHost(): void
+ {
+ $this->helper->setServerUrl('http://sub.example.org');
+
+ $expected = $this->getExpectedFileContents('sitemap/serverurl1.xml');
+ $this->assertEquals(trim($expected), $this->helper->render());
+ }
+
+ public function testSetServerUrlWithSchemeAndPortAndHostAndPath(): void
+ {
+ $this->helper->setServerUrl('http://sub.example.org:8080/foo/');
+
+ $expected = $this->getExpectedFileContents('sitemap/serverurl2.xml');
+ $this->assertEquals(trim($expected), $this->helper->render());
+ }
+
+ public function testGetUserSchemaValidation(): void
+ {
+ $this->helper->setUseSchemaValidation(true);
+ $this->assertTrue($this->helper->getUseSchemaValidation());
+ $this->helper->setUseSchemaValidation(false);
+ $this->assertFalse($this->helper->getUseSchemaValidation());
+ }
+
+ public function testUseSchemaValidation(): void
+ {
+ $this->markTestSkipped('Skipped because it fetches XSD from web');
+
+// $nav = clone $this->_nav2;
+// $this->_helper->setUseSitemapValidators(false);
+// $this->_helper->setUseSchemaValidation(true);
+// $nav->addPage(['label' => 'Invalid', 'uri' => 'http://w.']);
+//
+// try {
+// $this->_helper->render($nav);
+// } catch (View\Exception\ExceptionInterface $e) {
+// $expected = sprintf(
+// 'Sitemap is invalid according to XML Schema at "%s"',
+// Sitemap::SITEMAP_XSD
+// );
+// $actual = $e->getMessage();
+// $this->assertEquals($expected, $actual);
+// return;
+// }
+//
+// $this->fail('A Laminas\View\Exception\InvalidArgumentException was not thrown when using Schema validation');
+ }
+}
diff --git a/test/View/Helper/_files/expected/bc/acl.html b/test/View/Helper/_files/expected/bc/acl.html
new file mode 100644
index 00000000..e44ba374
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/acl.html
@@ -0,0 +1 @@
+Page 2 > Page 2.2 > Page 2.2.2
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/bc/default.html b/test/View/Helper/_files/expected/bc/default.html
new file mode 100644
index 00000000..d8601875
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/default.html
@@ -0,0 +1 @@
+Page 2 > Page 2.3 > Page 2.3.3 > Page 2.3.3.1
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/bc/linklast.html b/test/View/Helper/_files/expected/bc/linklast.html
new file mode 100644
index 00000000..b3e103a6
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/linklast.html
@@ -0,0 +1 @@
+Page 2 > Page 2.3 > Page 2.3.3 > Page 2.3.3.1
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/bc/maxdepth.html b/test/View/Helper/_files/expected/bc/maxdepth.html
new file mode 100644
index 00000000..ce8e36b5
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/maxdepth.html
@@ -0,0 +1 @@
+Page 2 > Page 2.3
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/bc/partial.html b/test/View/Helper/_files/expected/bc/partial.html
new file mode 100644
index 00000000..bb7f30e1
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/partial.html
@@ -0,0 +1 @@
+Page 2, Page 2.3, Page 2.3.3, Page 2.3.3.1
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/bc/partial_with_params.html b/test/View/Helper/_files/expected/bc/partial_with_params.html
new file mode 100644
index 00000000..7f33c62e
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/partial_with_params.html
@@ -0,0 +1,2 @@
+test value
+Page 2 / Page 2.3 / Page 2.3.3 / Page 2.3.3.1
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/bc/partialwithseparator.html b/test/View/Helper/_files/expected/bc/partialwithseparator.html
new file mode 100644
index 00000000..6b2b4c38
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/partialwithseparator.html
@@ -0,0 +1 @@
+Page 2 / Page 2.3 / Page 2.3.3 / Page 2.3.3.1
diff --git a/test/View/Helper/_files/expected/bc/separator.html b/test/View/Helper/_files/expected/bc/separator.html
new file mode 100644
index 00000000..bcb0f5d6
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/separator.html
@@ -0,0 +1 @@
+Page 2 fooPage 2.3 fooPage 2.3.3 fooPage 2.3.3.1
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/bc/textdomain.html b/test/View/Helper/_files/expected/bc/textdomain.html
new file mode 100644
index 00000000..5534fe6f
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/textdomain.html
@@ -0,0 +1 @@
+TextDomain1 2 > Page 2.3 > TextDomain1 2.3.3 > TextDomain2 2.3.3.1
diff --git a/test/View/Helper/_files/expected/bc/translated.html b/test/View/Helper/_files/expected/bc/translated.html
new file mode 100644
index 00000000..269228c9
--- /dev/null
+++ b/test/View/Helper/_files/expected/bc/translated.html
@@ -0,0 +1 @@
+Side 2 > Side 2.3 > Page 2.3.3 > Side 2.3.3.1
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/links/default.html b/test/View/Helper/_files/expected/links/default.html
new file mode 100644
index 00000000..f4ce1e3a
--- /dev/null
+++ b/test/View/Helper/_files/expected/links/default.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/acl.html b/test/View/Helper/_files/expected/menu/acl.html
new file mode 100644
index 00000000..91a92549
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/acl.html
@@ -0,0 +1,65 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/acl_role_interface.html b/test/View/Helper/_files/expected/menu/acl_role_interface.html
new file mode 100644
index 00000000..9d07eaa3
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/acl_role_interface.html
@@ -0,0 +1,62 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/acl_string.html b/test/View/Helper/_files/expected/menu/acl_string.html
new file mode 100644
index 00000000..9d07eaa3
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/acl_string.html
@@ -0,0 +1,62 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/addclasstolistitem_as_false.html b/test/View/Helper/_files/expected/menu/addclasstolistitem_as_false.html
new file mode 100644
index 00000000..e86b0fae
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/addclasstolistitem_as_false.html
@@ -0,0 +1,14 @@
+
diff --git a/test/View/Helper/_files/expected/menu/addclasstolistitem_as_true.html b/test/View/Helper/_files/expected/menu/addclasstolistitem_as_true.html
new file mode 100644
index 00000000..e6550283
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/addclasstolistitem_as_true.html
@@ -0,0 +1,14 @@
+
diff --git a/test/View/Helper/_files/expected/menu/bothdepts.html b/test/View/Helper/_files/expected/menu/bothdepts.html
new file mode 100644
index 00000000..4080f3e3
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/bothdepts.html
@@ -0,0 +1,52 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/css.html b/test/View/Helper/_files/expected/menu/css.html
new file mode 100644
index 00000000..8de4109c
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/css.html
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/css2.html b/test/View/Helper/_files/expected/menu/css2.html
new file mode 100644
index 00000000..f8cc4c02
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/css2.html
@@ -0,0 +1,11 @@
+
diff --git a/test/View/Helper/_files/expected/menu/default1.html b/test/View/Helper/_files/expected/menu/default1.html
new file mode 100644
index 00000000..c397849f
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/default1.html
@@ -0,0 +1,81 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/default2.html b/test/View/Helper/_files/expected/menu/default2.html
new file mode 100644
index 00000000..183f5f2a
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/default2.html
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/escapelabels_as_false.html b/test/View/Helper/_files/expected/menu/escapelabels_as_false.html
new file mode 100644
index 00000000..4f40aea6
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/escapelabels_as_false.html
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/escapelabels_as_true.html b/test/View/Helper/_files/expected/menu/escapelabels_as_true.html
new file mode 100644
index 00000000..d7b56b10
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/escapelabels_as_true.html
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/indent4.html b/test/View/Helper/_files/expected/menu/indent4.html
new file mode 100644
index 00000000..553e0731
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/indent4.html
@@ -0,0 +1,81 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/indent8.html b/test/View/Helper/_files/expected/menu/indent8.html
new file mode 100644
index 00000000..1a75d7ce
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/indent8.html
@@ -0,0 +1,81 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/maxdepth.html b/test/View/Helper/_files/expected/menu/maxdepth.html
new file mode 100644
index 00000000..1731bdac
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/maxdepth.html
@@ -0,0 +1,44 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/mindepth.html b/test/View/Helper/_files/expected/menu/mindepth.html
new file mode 100644
index 00000000..94cf81ef
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/mindepth.html
@@ -0,0 +1,60 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/onlyactivebranch.html b/test/View/Helper/_files/expected/menu/onlyactivebranch.html
new file mode 100644
index 00000000..49d98838
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/onlyactivebranch.html
@@ -0,0 +1,31 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/onlyactivebranch_addclasstolistitem.html b/test/View/Helper/_files/expected/menu/onlyactivebranch_addclasstolistitem.html
new file mode 100644
index 00000000..9514e408
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/onlyactivebranch_addclasstolistitem.html
@@ -0,0 +1,11 @@
+
diff --git a/test/View/Helper/_files/expected/menu/onlyactivebranch_bothdepts.html b/test/View/Helper/_files/expected/menu/onlyactivebranch_bothdepts.html
new file mode 100644
index 00000000..9b94e88f
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/onlyactivebranch_bothdepts.html
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/onlyactivebranch_maxdepth.html b/test/View/Helper/_files/expected/menu/onlyactivebranch_maxdepth.html
new file mode 100644
index 00000000..0e181df7
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/onlyactivebranch_maxdepth.html
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/onlyactivebranch_mindepth.html b/test/View/Helper/_files/expected/menu/onlyactivebranch_mindepth.html
new file mode 100644
index 00000000..56c86efe
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/onlyactivebranch_mindepth.html
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/onlyactivebranch_noparents.html b/test/View/Helper/_files/expected/menu/onlyactivebranch_noparents.html
new file mode 100644
index 00000000..b4eda0b1
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/onlyactivebranch_noparents.html
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/onlyactivebranch_np_bd.html b/test/View/Helper/_files/expected/menu/onlyactivebranch_np_bd.html
new file mode 100644
index 00000000..4e6690b4
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/onlyactivebranch_np_bd.html
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/onlyactivebranch_np_bd2.html b/test/View/Helper/_files/expected/menu/onlyactivebranch_np_bd2.html
new file mode 100644
index 00000000..a6ba4c07
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/onlyactivebranch_np_bd2.html
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/partial.html b/test/View/Helper/_files/expected/menu/partial.html
new file mode 100644
index 00000000..356c5a19
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/partial.html
@@ -0,0 +1,2 @@
+Is a container: yes
+Pages: Home, Page 1, Page 2, Page 3, Zym
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/partial_with_params.html b/test/View/Helper/_files/expected/menu/partial_with_params.html
new file mode 100644
index 00000000..9edc0b02
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/partial_with_params.html
@@ -0,0 +1,3 @@
+test value
+Is a container: yes
+Pages: Home, Page 1, Page 2, Page 3, Zym
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/menu/textdomain.html b/test/View/Helper/_files/expected/menu/textdomain.html
new file mode 100644
index 00000000..1ef39baa
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/textdomain.html
@@ -0,0 +1,48 @@
+
diff --git a/test/View/Helper/_files/expected/menu/translated.html b/test/View/Helper/_files/expected/menu/translated.html
new file mode 100644
index 00000000..b024da3b
--- /dev/null
+++ b/test/View/Helper/_files/expected/menu/translated.html
@@ -0,0 +1,81 @@
+
\ No newline at end of file
diff --git a/test/View/Helper/_files/expected/sitemap/acl.xml b/test/View/Helper/_files/expected/sitemap/acl.xml
new file mode 100644
index 00000000..97dad864
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/acl.xml
@@ -0,0 +1,54 @@
+
+
+
+ http://localhost/index
+
+
+ http://localhost/page1
+
+
+ http://localhost/page1/page1_1
+
+
+ http://localhost/page2
+
+
+ http://localhost/page2/page2_1
+
+
+ http://localhost/page2/page2_2
+
+
+ http://localhost/page2/page2_2/page2_2_1
+
+
+ http://localhost/page2/page2_2/page2_2_2
+
+
+ http://localhost/page2/page2_3
+
+
+ http://localhost/page2/page2_3/page2_3_1
+
+
+ http://localhost/page3
+
+
+ http://localhost/page3/page3_1
+
+
+ http://localhost/page3/page3_2
+
+
+ http://localhost/page3/page3_2/page3_2_1
+
+
+ http://localhost/page3/page3_2/page3_2_2
+
+
+ http://localhost/page3/page3_3
+
+
+ http://www.zym-project.com/
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/acl2.xml b/test/View/Helper/_files/expected/sitemap/acl2.xml
new file mode 100644
index 00000000..bdb75faf
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/acl2.xml
@@ -0,0 +1,39 @@
+
+
+
+ http://localhost/index
+
+
+ http://localhost/page1
+
+
+ http://localhost/page1/page1_1
+
+
+ http://localhost/page2
+
+
+ http://localhost/page2/page2_1
+
+
+ http://localhost/page2/page2_2
+
+
+ http://localhost/page2/page2_2/page2_2_1
+
+
+ http://localhost/page2/page2_2/page2_2_2
+
+
+ http://localhost/page2/page2_3
+
+
+ http://localhost/page2/page2_3/page2_3_1
+
+
+ http://localhost/page3
+
+
+ http://www.zym-project.com/
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/default1.xml b/test/View/Helper/_files/expected/sitemap/default1.xml
new file mode 100644
index 00000000..217f269b
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/default1.xml
@@ -0,0 +1,66 @@
+
+
+
+ http://localhost/index
+
+
+ http://localhost/page1
+
+
+ http://localhost/page1/page1_1
+
+
+ http://localhost/page2
+
+
+ http://localhost/page2/page2_1
+
+
+ http://localhost/page2/page2_2
+
+
+ http://localhost/page2/page2_2/page2_2_1
+
+
+ http://localhost/page2/page2_2/page2_2_2
+
+
+ http://localhost/page2/page2_3
+
+
+ http://localhost/page2/page2_3/page2_3_1
+
+
+ http://localhost/page2/page2_3/page2_3_3
+
+
+ http://localhost/page2/page2_3/page2_3_3/1
+
+
+ http://localhost/page2/page2_3/page2_3_3/2
+
+
+ http://localhost/page3
+
+
+ http://localhost/page3/page3_1
+
+
+ http://localhost/page3/page3_2
+
+
+ http://localhost/page3/page3_2/page3_2_1
+
+
+ http://localhost/page3/page3_2/page3_2_2
+
+
+ http://localhost/page3/page3_3
+
+
+ http://localhost/page3/page3_3/page3_3_2
+
+
+ http://www.zym-project.com/
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/default2.xml b/test/View/Helper/_files/expected/sitemap/default2.xml
new file mode 100644
index 00000000..e3c8d78a
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/default2.xml
@@ -0,0 +1,14 @@
+
+
+
+ http://localhost/site1
+ daily
+ 0.9
+
+
+ http://localhost/site2
+
+
+ http://localhost/site3
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/depth1.xml b/test/View/Helper/_files/expected/sitemap/depth1.xml
new file mode 100644
index 00000000..3da124d5
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/depth1.xml
@@ -0,0 +1,18 @@
+
+
+
+ http://localhost/index
+
+
+ http://localhost/page1
+
+
+ http://localhost/page2
+
+
+ http://localhost/page3
+
+
+ http://www.zym-project.com/
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/depth2.xml b/test/View/Helper/_files/expected/sitemap/depth2.xml
new file mode 100644
index 00000000..4b0e9fa7
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/depth2.xml
@@ -0,0 +1,51 @@
+
+
+
+ http://localhost/page1/page1_1
+
+
+ http://localhost/page2/page2_1
+
+
+ http://localhost/page2/page2_2
+
+
+ http://localhost/page2/page2_2/page2_2_1
+
+
+ http://localhost/page2/page2_2/page2_2_2
+
+
+ http://localhost/page2/page2_3
+
+
+ http://localhost/page2/page2_3/page2_3_1
+
+
+ http://localhost/page2/page2_3/page2_3_3
+
+
+ http://localhost/page2/page2_3/page2_3_3/1
+
+
+ http://localhost/page2/page2_3/page2_3_3/2
+
+
+ http://localhost/page3/page3_1
+
+
+ http://localhost/page3/page3_2
+
+
+ http://localhost/page3/page3_2/page3_2_1
+
+
+ http://localhost/page3/page3_2/page3_2_2
+
+
+ http://localhost/page3/page3_3
+
+
+ http://localhost/page3/page3_3/page3_3_2
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/depth3.xml b/test/View/Helper/_files/expected/sitemap/depth3.xml
new file mode 100644
index 00000000..78556a08
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/depth3.xml
@@ -0,0 +1,45 @@
+
+
+
+ http://localhost/page1/page1_1
+
+
+ http://localhost/page2/page2_1
+
+
+ http://localhost/page2/page2_2
+
+
+ http://localhost/page2/page2_2/page2_2_1
+
+
+ http://localhost/page2/page2_2/page2_2_2
+
+
+ http://localhost/page2/page2_3
+
+
+ http://localhost/page2/page2_3/page2_3_1
+
+
+ http://localhost/page2/page2_3/page2_3_3
+
+
+ http://localhost/page3/page3_1
+
+
+ http://localhost/page3/page3_2
+
+
+ http://localhost/page3/page3_2/page3_2_1
+
+
+ http://localhost/page3/page3_2/page3_2_2
+
+
+ http://localhost/page3/page3_3
+
+
+ http://localhost/page3/page3_3/page3_3_2
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/invalid.xml b/test/View/Helper/_files/expected/sitemap/invalid.xml
new file mode 100644
index 00000000..2d2e82ee
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/invalid.xml
@@ -0,0 +1,19 @@
+
+
+
+ http://localhost/site1
+ daily
+ 0.9
+
+
+ http://localhost/site2
+
+
+
+ http://localhost/site3
+ often
+
+
+ http://w.
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/nodecl.xml b/test/View/Helper/_files/expected/sitemap/nodecl.xml
new file mode 100644
index 00000000..329eebd1
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/nodecl.xml
@@ -0,0 +1,13 @@
+
+
+ http://localhost/site1
+ daily
+ 0.9
+
+
+ http://localhost/site2
+
+
+ http://localhost/site3
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/serverurl1.xml b/test/View/Helper/_files/expected/sitemap/serverurl1.xml
new file mode 100644
index 00000000..c10735e7
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/serverurl1.xml
@@ -0,0 +1,66 @@
+
+
+
+ http://sub.example.org/index
+
+
+ http://sub.example.org/page1
+
+
+ http://sub.example.org/page1/page1_1
+
+
+ http://sub.example.org/page2
+
+
+ http://sub.example.org/page2/page2_1
+
+
+ http://sub.example.org/page2/page2_2
+
+
+ http://sub.example.org/page2/page2_2/page2_2_1
+
+
+ http://sub.example.org/page2/page2_2/page2_2_2
+
+
+ http://sub.example.org/page2/page2_3
+
+
+ http://sub.example.org/page2/page2_3/page2_3_1
+
+
+ http://sub.example.org/page2/page2_3/page2_3_3
+
+
+ http://sub.example.org/page2/page2_3/page2_3_3/1
+
+
+ http://sub.example.org/page2/page2_3/page2_3_3/2
+
+
+ http://sub.example.org/page3
+
+
+ http://sub.example.org/page3/page3_1
+
+
+ http://sub.example.org/page3/page3_2
+
+
+ http://sub.example.org/page3/page3_2/page3_2_1
+
+
+ http://sub.example.org/page3/page3_2/page3_2_2
+
+
+ http://sub.example.org/page3/page3_3
+
+
+ http://sub.example.org/page3/page3_3/page3_3_2
+
+
+ http://www.zym-project.com/
+
+
diff --git a/test/View/Helper/_files/expected/sitemap/serverurl2.xml b/test/View/Helper/_files/expected/sitemap/serverurl2.xml
new file mode 100644
index 00000000..58a7a33a
--- /dev/null
+++ b/test/View/Helper/_files/expected/sitemap/serverurl2.xml
@@ -0,0 +1,66 @@
+
+
+
+ http://sub.example.org:8080/index
+
+
+ http://sub.example.org:8080/page1
+
+
+ http://sub.example.org:8080/page1/page1_1
+
+
+ http://sub.example.org:8080/page2
+
+
+ http://sub.example.org:8080/page2/page2_1
+
+
+ http://sub.example.org:8080/page2/page2_2
+
+
+ http://sub.example.org:8080/page2/page2_2/page2_2_1
+
+
+ http://sub.example.org:8080/page2/page2_2/page2_2_2
+
+
+ http://sub.example.org:8080/page2/page2_3
+
+
+ http://sub.example.org:8080/page2/page2_3/page2_3_1
+
+
+ http://sub.example.org:8080/page2/page2_3/page2_3_3
+
+
+ http://sub.example.org:8080/page2/page2_3/page2_3_3/1
+
+
+ http://sub.example.org:8080/page2/page2_3/page2_3_3/2
+
+
+ http://sub.example.org:8080/page3
+
+
+ http://sub.example.org:8080/page3/page3_1
+
+
+ http://sub.example.org:8080/page3/page3_2
+
+
+ http://sub.example.org:8080/page3/page3_2/page3_2_1
+
+
+ http://sub.example.org:8080/page3/page3_2/page3_2_2
+
+
+ http://sub.example.org:8080/page3/page3_3
+
+
+ http://sub.example.org:8080/page3/page3_3/page3_3_2
+
+
+ http://www.zym-project.com/
+
+
diff --git a/test/View/Helper/_files/mvc/views/bc.phtml b/test/View/Helper/_files/mvc/views/bc.phtml
new file mode 100644
index 00000000..383d06af
--- /dev/null
+++ b/test/View/Helper/_files/mvc/views/bc.phtml
@@ -0,0 +1,10 @@
+getLabel();
+ },
+ $this->vars()->pages
+ )
+);
diff --git a/test/View/Helper/_files/mvc/views/bc_separator.phtml b/test/View/Helper/_files/mvc/views/bc_separator.phtml
new file mode 100644
index 00000000..89fce118
--- /dev/null
+++ b/test/View/Helper/_files/mvc/views/bc_separator.phtml
@@ -0,0 +1,10 @@
+vars()->separator,
+ array_map(
+ function($a) {
+ return $a->getLabel();
+ },
+ $this->vars()->pages
+ )
+);
diff --git a/test/View/Helper/_files/mvc/views/bc_with_partial_params.phtml b/test/View/Helper/_files/mvc/views/bc_with_partial_params.phtml
new file mode 100644
index 00000000..c75d92f2
--- /dev/null
+++ b/test/View/Helper/_files/mvc/views/bc_with_partial_params.phtml
@@ -0,0 +1,11 @@
+variable . PHP_EOL;
+echo implode(
+ $this->vars()->separator,
+ array_map(
+ function($a) {
+ return $a->getLabel();
+ },
+ $this->vars()->pages
+ )
+);
diff --git a/test/View/Helper/_files/mvc/views/menu.phtml b/test/View/Helper/_files/mvc/views/menu.phtml
new file mode 100644
index 00000000..7b45c362
--- /dev/null
+++ b/test/View/Helper/_files/mvc/views/menu.phtml
@@ -0,0 +1,13 @@
+vars('container') instanceof \Laminas\Navigation\AbstractContainer
+ ? 'yes'
+ : 'no';
+
+$pages = array();
+foreach ($this->vars('container') as $page) {
+ $pages[] = $page->getLabel();
+}
+$pages = implode(', ', $pages);
+?>
+Is a container:
+Pages:
diff --git a/test/View/Helper/_files/mvc/views/menu_with_partial_params.phtml b/test/View/Helper/_files/mvc/views/menu_with_partial_params.phtml
new file mode 100644
index 00000000..c75e759d
--- /dev/null
+++ b/test/View/Helper/_files/mvc/views/menu_with_partial_params.phtml
@@ -0,0 +1,13 @@
+vars('container') instanceof \Laminas\Navigation\AbstractContainer
+ ? 'yes'
+ : 'no';
+echo $this->variable . PHP_EOL;
+$pages = array();
+foreach ($this->vars('container') as $page) {
+ $pages[] = $page->getLabel();
+}
+$pages = implode(', ', $pages);
+?>
+Is a container:
+Pages:
diff --git a/test/View/Helper/_files/navigation-config.php b/test/View/Helper/_files/navigation-config.php
new file mode 100644
index 00000000..fe7ff8bc
--- /dev/null
+++ b/test/View/Helper/_files/navigation-config.php
@@ -0,0 +1,264 @@
+ [
+ 'zym' => [
+ 'label' => 'Zym',
+ 'uri' => 'http://www.zym-project.com/',
+ 'order' => '100',
+ ],
+ 'page1' => [
+ 'label' => 'Page 1',
+ 'uri' => 'page1',
+ 'pages' => [
+ 'page1_1' => [
+ 'label' => 'Page 1.1',
+ 'uri' => 'page1/page1_1',
+ ],
+ ],
+ ],
+ 'page2' => [
+ 'label' => 'Page 2',
+ 'uri' => 'page2',
+ 'pages' => [
+ 'page2_1' => [
+ 'label' => 'Page 2.1',
+ 'uri' => 'page2/page2_1',
+ ],
+ 'page2_2' => [
+ 'label' => 'Page 2.2',
+ 'uri' => 'page2/page2_2',
+ 'pages' => [
+ 'page2_2_1' => [
+ 'label' => 'Page 2.2.1',
+ 'uri' => 'page2/page2_2/page2_2_1',
+ ],
+ 'page2_2_2' => [
+ 'label' => 'Page 2.2.2',
+ 'uri' => 'page2/page2_2/page2_2_2',
+ 'active' => '1',
+ ],
+ ],
+ ],
+ 'page2_3' => [
+ 'label' => 'Page 2.3',
+ 'uri' => 'page2/page2_3',
+ 'pages' => [
+ 'page2_3_1' => [
+ 'label' => 'Page 2.3.1',
+ 'uri' => 'page2/page2_3/page2_3_1',
+ ],
+ 'page2_3_2' => [
+ 'label' => 'Page 2.3.2',
+ 'uri' => 'page2/page2_3/page2_3_2',
+ 'visible' => '0',
+ 'pages' => [
+ 'page2_3_2_1' => [
+ 'label' => 'Page 2.3.2.1',
+ 'uri' => 'page2/page2_3/page2_3_2/1',
+ 'active' => '1',
+ ],
+ 'page2_3_2_2' => [
+ 'label' => 'Page 2.3.2.2',
+ 'uri' => 'page2/page2_3/page2_3_2/2',
+ 'active' => '1',
+ 'pages' => [
+ 'page_2_3_2_2_1' => [
+ 'label' => 'Ignore',
+ 'uri' => '#',
+ 'active' => '1',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'page2_3_3' => [
+ 'label' => 'Page 2.3.3',
+ 'uri' => 'page2/page2_3/page2_3_3',
+ 'resource' => 'admin_foo',
+ 'pages' => [
+ 'page2_3_3_1' => [
+ 'label' => 'Page 2.3.3.1',
+ 'uri' => 'page2/page2_3/page2_3_3/1',
+ 'active' => '1',
+ ],
+ 'page2_3_3_2' => [
+ 'label' => 'Page 2.3.3.2',
+ 'uri' => 'page2/page2_3/page2_3_3/2',
+ 'resource' => 'guest_foo',
+ 'active' => '1',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'page3' => [
+ 'label' => 'Page 3',
+ 'uri' => 'page3',
+ 'pages' => [
+ 'page3_1' => [
+ 'label' => 'Page 3.1',
+ 'uri' => 'page3/page3_1',
+ 'resource' => 'guest_foo',
+ ],
+ 'page3_2' => [
+ 'label' => 'Page 3.2',
+ 'uri' => 'page3/page3_2',
+ 'resource' => 'member_foo',
+ 'pages' => [
+ 'page3_2_1' => [
+ 'label' => 'Page 3.2.1',
+ 'uri' => 'page3/page3_2/page3_2_1',
+ ],
+ 'page3_2_2' => [
+ 'label' => 'Page 3.2.2',
+ 'uri' => 'page3/page3_2/page3_2_2',
+ 'resource' => 'admin_foo',
+ 'privilege' => 'read',
+ ],
+ ],
+ ],
+ 'page3_3' => [
+ 'label' => 'Page 3.3',
+ 'uri' => 'page3/page3_3',
+ 'resource' => 'special_foo',
+ 'pages' => [
+ 'page3_3_1' => [
+ 'label' => 'Page 3.3.1',
+ 'uri' => 'page3/page3_3/page3_3_1',
+ 'visible' => '0',
+ ],
+ 'page3_3_2' => [
+ 'label' => 'Page 3.3.2',
+ 'uri' => 'page3/page3_3/page3_3_2',
+ 'resource' => 'admin_foo',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'home' => [
+ 'label' => 'Home',
+ 'uri' => 'index',
+ 'title' => 'Go home',
+ 'order' => '-100',
+ ],
+ ],
+ 'nav_test2' => [
+ 'site1' => [
+ 'label' => 'Site 1',
+ 'uri' => 'site1',
+ 'changefreq' => 'daily',
+ 'priority' => '0.9',
+ ],
+ 'site2' => [
+ 'label' => 'Site 2',
+ 'uri' => 'site2',
+ 'active' => '1',
+ 'lastmod' => 'earlier',
+ ],
+ 'site3' => [
+ 'label' => 'Site 3',
+ 'uri' => 'site3',
+ 'changefreq' => 'often',
+ ],
+ ],
+ 'nav_test3' => [
+ 'page1' => [
+ 'label' => 'Page 1',
+ 'uri' => 'page1',
+ 'pages' => [
+ 'page1_1' => [
+ 'label' => 'Page 1.1',
+ 'uri' => 'page1/page1_1',
+ 'textdomain' => 'LaminasTest_1',
+ ],
+ ],
+ ],
+ 'page2' => [
+ 'label' => 'Page 2',
+ 'uri' => 'page2',
+ 'textdomain' => 'LaminasTest_1',
+ 'pages' => [
+ 'page2_1' => [
+ 'label' => 'Page 2.1',
+ 'uri' => 'page2/page2_1',
+ ],
+ 'page2_2' => [
+ 'label' => 'Page 2.2',
+ 'uri' => 'page2/page2_2',
+ 'pages' => [
+ 'page2_2_1' => [
+ 'label' => 'Page 2.2.1',
+ 'uri' => 'page2/page2_2/page2_2_1',
+ ],
+ 'page2_2_2' => [
+ 'label' => 'Page 2.2.2',
+ 'uri' => 'page2/page2_2/page2_2_2',
+ 'active' => '1',
+ ],
+ ],
+ ],
+ 'page2_3' => [
+ 'label' => 'Page 2.3',
+ 'uri' => 'page2/page2_3',
+ 'textdomain' => 'LaminasTest_No',
+ 'pages' => [
+ 'page2_3_1' => [
+ 'label' => 'Page 2.3.1',
+ 'uri' => 'page2/page2_3/page2_3_1',
+ ],
+ 'page2_3_2' => [
+ 'label' => 'Page 2.3.2',
+ 'uri' => 'page2/page2_3/page2_3_2',
+ 'visible' => '0',
+ 'pages' => [
+ 'page2_3_2_1' => [
+ 'label' => 'Page 2.3.2.1',
+ 'uri' => 'page2/page2_3/page2_3_2/1',
+ 'active' => '1',
+ ],
+ 'page2_3_2_2' => [
+ 'label' => 'Page 2.3.2.2',
+ 'uri' => 'page2/page2_3/page2_3_2/2',
+ 'active' => '1',
+ 'pages' => [
+ 'page_2_3_2_2_1' => [
+ 'label' => 'Ignore',
+ 'uri' => '#',
+ 'active' => '1',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'page2_3_3' => [
+ 'label' => 'Page 2.3.3',
+ 'uri' => 'page2/page2_3/page2_3_3',
+ 'resource' => 'admin_foo',
+ 'textdomain' => 'LaminasTest_1',
+ 'pages' => [
+ 'page2_3_3_1' => [
+ 'label' => 'Page 2.3.3.1',
+ 'uri' => 'page2/page2_3/page2_3_3/1',
+ 'active' => '1',
+ 'textdomain' => 'LaminasTest_2',
+ ],
+ 'page2_3_3_2' => [
+ 'label' => 'Page 2.3.3.2',
+ 'uri' => 'page2/page2_3/page2_3_3/2',
+ 'resource' => 'guest_foo',
+ 'active' => '1',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+];
diff --git a/test/View/HelperConfigTest.php b/test/View/HelperConfigTest.php
index 191f0a05..47bbdc6a 100644
--- a/test/View/HelperConfigTest.php
+++ b/test/View/HelperConfigTest.php
@@ -5,9 +5,10 @@
namespace LaminasTest\Navigation\View;
use Laminas\Navigation\Service\DefaultNavigationFactory;
+use Laminas\Navigation\View\Helper\Links;
+use Laminas\Navigation\View\Helper\Navigation;
use Laminas\Navigation\View\HelperConfig;
use Laminas\ServiceManager\ServiceManager;
-use Laminas\View\Helper\Navigation as NavigationHelper;
use Laminas\View\HelperPluginManager;
use PHPUnit\Framework\TestCase;
@@ -24,7 +25,7 @@ public static function navigationServiceNameProvider(): array
return [
['navigation'],
['Navigation'],
- [NavigationHelper::class],
+ [Navigation::class],
['laminasviewhelpernavigation'],
];
}
@@ -34,8 +35,8 @@ public static function navigationServiceNameProvider(): array
*/
public function testConfigureServiceManagerWithConfig(
string $navigationHelperServiceName
- ) {
- $replacedMenuClass = NavigationHelper\Links::class;
+ ): void {
+ $replacedMenuClass = Links::class;
$serviceManager = new ServiceManager([
'services' => [
diff --git a/test/View/ViewHelperManagerDelegatorFactoryTest.php b/test/View/ViewHelperManagerDelegatorFactoryTest.php
index b9a7b776..7ea84a73 100644
--- a/test/View/ViewHelperManagerDelegatorFactoryTest.php
+++ b/test/View/ViewHelperManagerDelegatorFactoryTest.php
@@ -4,9 +4,9 @@
namespace LaminasTest\Navigation\View;
+use Laminas\Navigation\View\Helper\Navigation;
use Laminas\Navigation\View\ViewHelperManagerDelegatorFactory;
use Laminas\ServiceManager\ServiceManager;
-use Laminas\View\Helper\Navigation as NavigationHelper;
use Laminas\View\HelperPluginManager;
use PHPUnit\Framework\TestCase;
@@ -22,6 +22,6 @@ public function testFactoryConfiguresViewHelperManagerWithNavigationHelpers(): v
$this->assertSame($helpers, $factory($services, 'ViewHelperManager', $callback));
$this->assertTrue($helpers->has('navigation'));
- $this->assertTrue($helpers->has(NavigationHelper::class));
+ $this->assertTrue($helpers->has(Navigation::class));
}
}