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 .= ''; + 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 = ''; + + $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 2fooPage 2.3fooPage 2.3.3fooPage 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)); } }