From 685b1c2597b98594a5d524ae7096cae189cee5a5 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Sat, 14 Feb 2026 09:53:17 +1100 Subject: [PATCH 1/4] Rebased with changes from #92 Signed-off-by: Simon Mundy --- .laminas-ci.json | 3 +- composer.json | 11 +- composer.lock | 2498 ++++++++--------- psalm-baseline.xml | 1070 ++----- psalm.xml.dist | 5 - src/AbstractRouteStack.php | 180 ++ src/ConfigProvider.php | 25 +- src/Container/AbstractRouteContainer.php | 153 + src/Container/HttpRouteContainer.php | 31 + src/Container/HttpRouteContainerInterface.php | 31 + src/Container/RouteContainerInterface.php | 47 + src/Container/SimpleRouteContainer.php | 30 + .../SimpleRouteContainerInterface.php | 30 + src/Exception/InvalidArgumentException.php | 2 +- src/Exception/RuntimeException.php | 2 +- src/Http/Chain.php | 145 +- src/Http/Hostname.php | 149 +- src/Http/HttpRouteInterface.php | 26 +- src/Http/HttpRouterFactory.php | 42 +- src/Http/Literal.php | 120 +- src/Http/Method.php | 123 +- src/Http/Part.php | 164 +- src/Http/Placeholder.php | 85 +- src/Http/Regex.php | 137 +- src/Http/RouteInterface.php | 18 - src/Http/RouteMatch.php | 24 +- src/Http/Scheme.php | 125 +- src/Http/Segment.php | 172 +- src/Http/TranslatorAwareTreeRouteStack.php | 97 +- src/Http/TreeRouteStack.php | 303 +- src/Http/Wildcard.php | 203 -- src/Module.php | 26 - src/PriorityList.php | 16 - src/RouteConfigTrait.php | 83 + src/RouteInterface.php | 32 +- src/RouteInvokableFactory.php | 57 +- src/RouteMatch.php | 35 +- src/RouteMatchInterface.php | 38 + src/RoutePluginManager.php | 137 +- src/RoutePluginManagerFactory.php | 40 +- src/RoutePriorityTrait.php | 20 + src/RouteStackInterface.php | 25 +- src/RouterConfigTrait.php | 15 +- src/RouterFactory.php | 11 +- src/SimpleRouteStack.php | 270 +- test/FactoryTester.php | 27 +- test/Http/ChainTest.php | 32 +- test/Http/HostnameTest.php | 49 +- test/Http/LiteralTest.php | 49 +- test/Http/MethodTest.php | 33 +- test/Http/PartTest.php | 208 +- test/Http/PlaceholderTest.php | 44 +- test/Http/RegexTest.php | 55 +- test/Http/SchemeTest.php | 28 +- test/Http/SegmentTest.php | 122 +- test/Http/TestAsset/DummyRoute.php | 42 +- test/Http/TestAsset/DummyRouteWithParam.php | 21 +- .../TranslatorAwareTreeRouteStackTest.php | 37 +- test/Http/TreeRouteStackTest.php | 228 +- test/Http/WildcardTest.php | 222 -- test/PriorityListTest.php | 118 - test/RouteContainerTest.php | 116 + test/RoutePluginManagerFactoryTest.php | 3 +- test/RouterFactoryTest.php | 37 +- test/SimpleRouteStackTest.php | 192 +- test/TestAsset/DummyRoute.php | 17 +- test/TestAsset/DummyRouteWithParam.php | 8 +- test/TestAsset/MockServerRequest.php | 417 +++ test/TestAsset/MockStream.php | 124 + test/TestAsset/MockUri.php | 146 + test/TestAsset/Router.php | 31 +- 71 files changed, 4138 insertions(+), 5124 deletions(-) create mode 100644 src/AbstractRouteStack.php create mode 100644 src/Container/AbstractRouteContainer.php create mode 100644 src/Container/HttpRouteContainer.php create mode 100644 src/Container/HttpRouteContainerInterface.php create mode 100644 src/Container/RouteContainerInterface.php create mode 100644 src/Container/SimpleRouteContainer.php create mode 100644 src/Container/SimpleRouteContainerInterface.php delete mode 100644 src/Http/RouteInterface.php delete mode 100644 src/Http/Wildcard.php delete mode 100644 src/Module.php delete mode 100644 src/PriorityList.php create mode 100644 src/RouteConfigTrait.php create mode 100644 src/RouteMatchInterface.php create mode 100644 src/RoutePriorityTrait.php delete mode 100644 test/Http/WildcardTest.php delete mode 100644 test/PriorityListTest.php create mode 100644 test/RouteContainerTest.php create mode 100644 test/TestAsset/MockServerRequest.php create mode 100644 test/TestAsset/MockStream.php create mode 100644 test/TestAsset/MockUri.php diff --git a/.laminas-ci.json b/.laminas-ci.json index be23f935..57d29d6b 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -1,6 +1,5 @@ { "ignore_php_platform_requirements": { "8.5": true - }, - "backwardCompatibilityCheck": true + } } diff --git a/composer.json b/composer.json index b451428f..0339491d 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "php": "8.2.99" }, "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "composer/package-versions-deprecated": true } }, "extra": { @@ -32,13 +33,13 @@ }, "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "laminas/laminas-servicemanager": "^3.14.0", - "laminas/laminas-stdlib": "^3.10.1" + "psr/http-message": "^2.0", + "laminas/laminas-servicemanager": "^4.5.0", + "laminas/laminas-uri": "^2.14" }, "require-dev": { + "laminas/laminas-translator": "^1.1", "laminas/laminas-coding-standard": "~3.1.0", - "laminas/laminas-http": "^2.22", - "laminas/laminas-i18n": "^2.31.0", "phpunit/phpunit": "^11.5.43", "psalm/plugin-phpunit": "^0.19.5", "vimeo/psalm": "^6.13.1" diff --git a/composer.lock b/composer.lock index 7833bf4c..249cbf00 100644 --- a/composer.lock +++ b/composer.lock @@ -4,63 +4,170 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "276131874803788de4f236050a851569", + "content-hash": "6f80c5b1e857d626afca3c5e36856ae8", "packages": [ + { + "name": "brick/varexporter", + "version": "0.6.0", + "source": { + "type": "git", + "url": "https://github.com/brick/varexporter.git", + "reference": "af98bfc2b702a312abbcaff37656dbe419cec5bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/varexporter/zipball/af98bfc2b702a312abbcaff37656dbe419cec5bc", + "reference": "af98bfc2b702a312abbcaff37656dbe419cec5bc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "6.8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\VarExporter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()", + "keywords": [ + "var_export" + ], + "support": { + "issues": "https://github.com/brick/varexporter/issues", + "source": "https://github.com/brick/varexporter/tree/0.6.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-02-20T17:42:39+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.18.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "06f211dfffff18d91844c1f55250d5d13c007e18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/06f211dfffff18d91844c1f55250d5d13c007e18", + "reference": "06f211dfffff18d91844c1f55250d5d13c007e18", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-escaper": "*" + }, + "require-dev": { + "infection/infection": "^0.31.0", + "laminas/laminas-coding-standard": "~3.1.0", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-14T18:31:13+00:00" + }, { "name": "laminas/laminas-servicemanager", - "version": "3.24.0", + "version": "4.5.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-servicemanager.git", - "reference": "b172a0df568bf37ebdfb3658263156eefe3c1e8c" + "reference": "a6996829c8ce55025cca1b57b1e8a8b165e3926c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/b172a0df568bf37ebdfb3658263156eefe3c1e8c", - "reference": "b172a0df568bf37ebdfb3658263156eefe3c1e8c", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/a6996829c8ce55025cca1b57b1e8a8b165e3926c", + "reference": "a6996829c8ce55025cca1b57b1e8a8b165e3926c", "shasum": "" }, "require": { + "brick/varexporter": "^0.3.8 || ^0.4.0 || ^0.5.0 || ^0.6.0", "laminas/laminas-stdlib": "^3.19", "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "psr/container": "^1.0" + "psr/container": "^1.1 || ^2.0" }, "conflict": { - "ext-psr": "*", "laminas/laminas-code": "<4.10.0", - "zendframework/zend-code": "<3.3.1", - "zendframework/zend-servicemanager": "*" + "zendframework/zend-code": "<3.3.1" }, "provide": { - "psr/container-implementation": "^1.0" - }, - "replace": { - "container-interop/container-interop": "^1.2.0" + "psr/container-implementation": "^1.0 || ^2.0" }, "require-dev": { "composer/package-versions-deprecated": "^1.11.99.5", "friendsofphp/proxy-manager-lts": "^1.0.18", - "laminas/laminas-code": "^4.16.0", - "laminas/laminas-coding-standard": "~2.5.0", - "laminas/laminas-container-config-test": "^0.8", + "laminas/laminas-cli": "^1.11", + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-container-config-test": "^1.1", "mikey179/vfsstream": "^1.6.12", "phpbench/phpbench": "^1.4.1", "phpunit/phpunit": "^10.5.58", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.26.1" + "psalm/plugin-phpunit": "^0.19.5", + "symfony/console": "^6.4.17 || ^7.3.4", + "vimeo/psalm": "^6.13.1" }, "suggest": { - "friendsofphp/proxy-manager-lts": "ProxyManager ^2.1.1 to handle lazy initialization of services" + "friendsofphp/proxy-manager-lts": "To handle lazy initialization of services", + "laminas/laminas-cli": "To consume CLI commands provided by this component" }, - "bin": [ - "bin/generate-deps-for-config-factory", - "bin/generate-factory-for-class" - ], "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\ServiceManager", + "config-provider": "Laminas\\ServiceManager\\ConfigProvider" + } + }, "autoload": { - "files": [ - "src/autoload.php" - ], "psr-4": { "Laminas\\ServiceManager\\": "src/" } @@ -82,11 +189,9 @@ ], "support": { "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-servicemanager/", "forum": "https://discourse.laminas.dev", "issues": "https://github.com/laminas/laminas-servicemanager/issues", - "rss": "https://github.com/laminas/laminas-servicemanager/releases.atom", - "source": "https://github.com/laminas/laminas-servicemanager" + "source": "https://github.com/laminas/laminas-servicemanager/tree/4.5.0" }, "funding": [ { @@ -94,7 +199,7 @@ "type": "community_bridge" } ], - "time": "2025-10-14T09:03:51+00:00" + "time": "2025-10-14T09:41:04+00:00" }, { "name": "laminas/laminas-stdlib", @@ -156,315 +261,278 @@ "time": "2025-10-11T18:13:12+00:00" }, { - "name": "psr/container", - "version": "1.1.2", + "name": "laminas/laminas-translator", + "version": "1.3.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + "url": "https://github.com/laminas/laminas-translator.git", + "reference": "1d6bf7d857c3fc310a30f5ea028731b9bf8be9db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "url": "https://api.github.com/repos/laminas/laminas-translator/zipball/1d6bf7d857c3fc310a30f5ea028731b9bf8be9db", + "reference": "1d6bf7d857c3fc310a30f5ea028731b9bf8be9db", "shasum": "" }, "require": { - "php": ">=7.4.0" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "phpunit/phpunit": "^11.5.46", + "vimeo/psalm": "^6.14.3" }, "type": "library", "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "Laminas\\Translator\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } + "BSD-3-Clause" ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Interfaces for the Translator component of laminas-i18n", + "homepage": "https://laminas.dev", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "i18n", + "laminas" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" + "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" }, - "time": "2021-11-05T16:50:12+00:00" - } - ], - "packages-dev": [ + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-12-30T14:26:45+00:00" + }, { - "name": "amphp/amp", - "version": "v3.1.1", + "name": "laminas/laminas-uri", + "version": "2.14.0", "source": { "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" + "url": "https://github.com/laminas/laminas-uri.git", + "reference": "e804288f4540988903dc0ede386ce5eec87198df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "url": "https://api.github.com/repos/laminas/laminas-uri/zipball/e804288f4540988903dc0ede386ce5eec87198df", + "reference": "e804288f4540988903dc0ede386ce5eec87198df", "shasum": "" }, "require": { - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" + "laminas/laminas-escaper": "^2.9", + "laminas/laminas-validator": "^2.39 || ^3.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-uri": "*" }, "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "5.23.1" + "laminas/laminas-coding-standard": "~2.4.0", + "phpunit/phpunit": "^11.0" }, "type": "library", "autoload": { - "files": [ - "src/functions.php", - "src/Future/functions.php", - "src/Internal/functions.php" - ], "psr-4": { - "Amp\\": "src" + "Laminas\\Uri\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - } + "BSD-3-Clause" ], - "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "https://amphp.org/amp", + "description": "A component that aids in manipulating and validating ยป Uniform Resource Identifiers (URIs)", + "homepage": "https://laminas.dev", "keywords": [ - "async", - "asynchronous", - "awaitable", - "concurrency", - "event", - "event-loop", - "future", - "non-blocking", - "promise" + "laminas", + "uri" ], "support": { - "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v3.1.1" + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-uri/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-uri/issues", + "rss": "https://github.com/laminas/laminas-uri/releases.atom", + "source": "https://github.com/laminas/laminas-uri" }, "funding": [ { - "url": "https://github.com/amphp", - "type": "github" + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" } ], - "time": "2025-08-27T21:42:00+00:00" + "time": "2025-12-05T10:02:11+00:00" }, { - "name": "amphp/byte-stream", - "version": "v2.1.2", + "name": "laminas/laminas-validator", + "version": "3.13.1", "source": { "type": "git", - "url": "https://github.com/amphp/byte-stream.git", - "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + "url": "https://github.com/laminas/laminas-validator.git", + "reference": "305d6101df90f9c51a69de274e06a8cd7d45ce1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", - "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/305d6101df90f9c51a69de274e06a8cd7d45ce1e", + "reference": "305d6101df90f9c51a69de274e06a8cd7d45ce1e", "shasum": "" }, "require": { - "amphp/amp": "^3", - "amphp/parser": "^1.1", - "amphp/pipeline": "^1", - "amphp/serialization": "^1", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2.3" + "ext-ctype": "*", + "ext-fileinfo": "*", + "ext-filter": "*", + "ext-intl": "*", + "laminas/laminas-servicemanager": "^4.1.0", + "laminas/laminas-stdlib": "^3.19", + "laminas/laminas-translator": "^1.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.1 || ^2.0", + "psr/http-client": "^1.0.3", + "psr/http-factory": "^1.1.0", + "psr/http-message": "^1.0.1 || ^2.0.0" }, "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.22.1" + "laminas/laminas-coding-standard": "^3.1.0", + "laminas/laminas-diactoros": "^3.8.0", + "phpunit/phpunit": "^10.5.60", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.14.2" + }, + "suggest": { + "laminas/laminas-i18n": "Laminas\\I18n component to allow translation of validation error messages", + "laminas/laminas-i18n-resources": "Translations of validator messages" }, "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Validator", + "config-provider": "Laminas\\Validator\\ConfigProvider" + } + }, "autoload": { - "files": [ - "src/functions.php", - "src/Internal/functions.php" - ], "psr-4": { - "Amp\\ByteStream\\": "src" + "Laminas\\Validator\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } + "BSD-3-Clause" ], - "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "https://amphp.org/byte-stream", + "description": "Validation classes for a wide range of domains, and the ability to chain validators to create complex validation criteria", + "homepage": "https://laminas.dev", "keywords": [ - "amp", - "amphp", - "async", - "io", - "non-blocking", - "stream" + "laminas", + "validator" ], "support": { - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-validator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-validator/issues", + "rss": "https://github.com/laminas/laminas-validator/releases.atom", + "source": "https://github.com/laminas/laminas-validator" }, "funding": [ { - "url": "https://github.com/amphp", - "type": "github" + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" } ], - "time": "2025-03-16T17:10:27+00:00" + "time": "2026-01-20T17:33:57+00:00" }, { - "name": "amphp/cache", - "version": "v2.0.1", + "name": "nikic/php-parser", + "version": "v5.7.0", "source": { "type": "git", - "url": "https://github.com/amphp/cache.git", - "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", - "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { - "amphp/amp": "^3", - "amphp/serialization": "^1", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, + "bin": [ + "bin/php-parse" + ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, "autoload": { "psr-4": { - "Amp\\Cache\\": "src" + "PhpParser\\": "lib/PhpParser" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" + "name": "Nikita Popov" } ], - "description": "A fiber-aware cache API based on Amp and Revolt.", - "homepage": "https://amphp.org/cache", + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], "support": { - "issues": "https://github.com/amphp/cache/issues", - "source": "https://github.com/amphp/cache/tree/v2.0.1" + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-19T03:38:06+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { - "name": "amphp/dns", - "version": "v2.4.0", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/amphp/dns.git", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/cache": "^2", - "amphp/parser": "^1", - "amphp/process": "^2", - "daverandom/libdns": "^2.0.2", - "ext-filter": "*", - "ext-json": "*", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.20" + "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { - "files": [ - "src/functions.php" - ], "psr-4": { - "Amp\\Dns\\": "src" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -473,91 +541,52 @@ ], "authors": [ { - "name": "Chris Wright", - "email": "addr@daverandom.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Async DNS resolution for Amp.", - "homepage": "https://github.com/amphp/dns", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "amp", - "amphp", - "async", - "client", - "dns", - "resolve" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "issues": "https://github.com/amphp/dns/issues", - "source": "https://github.com/amphp/dns/tree/v2.4.0" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-01-19T15:43:40+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { - "name": "amphp/parallel", - "version": "v2.3.3", + "name": "psr/http-client", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/amphp/parallel.git", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/cache": "^2", - "amphp/parser": "^1", - "amphp/pipeline": "^1", - "amphp/process": "^2", - "amphp/serialization": "^1", - "amphp/socket": "^2", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { - "files": [ - "src/Context/functions.php", - "src/Context/Internal/functions.php", - "src/Ipc/functions.php", - "src/Worker/functions.php" - ], "psr-4": { - "Amp\\Parallel\\": "src" + "Psr\\Http\\Client\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -566,65 +595,50 @@ ], "authors": [ { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Parallel processing component for Amp.", - "homepage": "https://github.com/amphp/parallel", + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", "keywords": [ - "async", - "asynchronous", - "concurrent", - "multi-processing", - "multi-threading" + "http", + "http-client", + "psr", + "psr-18" ], "support": { - "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.3" + "source": "https://github.com/php-fig/http-client" }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-11-15T06:23:42+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { - "name": "amphp/parser", - "version": "v1.1.1", + "name": "psr/http-factory", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/amphp/parser.git", - "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", - "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.4" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-4": { - "Amp\\Parser\\": "src" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -633,63 +647,52 @@ ], "authors": [ { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A generator parser to make streaming parsers simple.", - "homepage": "https://github.com/amphp/parser", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ - "async", - "non-blocking", - "parser", - "stream" + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" ], "support": { - "issues": "https://github.com/amphp/parser/issues", - "source": "https://github.com/amphp/parser/tree/v1.1.1" + "source": "https://github.com/php-fig/http-factory" }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-03-21T19:16:53+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { - "name": "amphp/pipeline", - "version": "v1.2.3", + "name": "psr/http-message", + "version": "2.0", "source": { "type": "git", - "url": "https://github.com/amphp/pipeline.git", - "reference": "7b52598c2e9105ebcddf247fc523161581930367" + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", - "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "amphp/amp": "^3", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "php": "^7.2 || ^8.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { - "Amp\\Pipeline\\": "src" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -698,70 +701,59 @@ ], "authors": [ { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Asynchronous iterators and operators.", - "homepage": "https://amphp.org/pipeline", + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", "keywords": [ - "amp", - "amphp", - "async", - "io", - "iterator", - "non-blocking" + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" ], "support": { - "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-03-16T16:33:53+00:00" - }, + "time": "2023-04-04T09:54:51+00:00" + } + ], + "packages-dev": [ { - "name": "amphp/process", - "version": "v2.0.3", + "name": "amphp/amp", + "version": "v3.1.1", "source": { "type": "git", - "url": "https://github.com/amphp/process.git", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + "url": "https://github.com/amphp/amp.git", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", "shasum": "" }, "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/sync": "^2", "php": ">=8.1", "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" + "psalm/phar": "5.23.1" }, "type": "library", "autoload": { "files": [ - "src/functions.php" + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "Amp\\Process\\": "src" + "Amp\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -769,24 +761,39 @@ "MIT" ], "authors": [ - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, { "name": "Aaron Piotrowski", "email": "aaron@trowski.com" }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, { "name": "Niklas Keller", "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], - "description": "A fiber-aware process manager based on Amp and Revolt.", - "homepage": "https://amphp.org/process", - "support": { - "issues": "https://github.com/amphp/process/issues", - "source": "https://github.com/amphp/process/tree/v2.0.3" + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.1" }, "funding": [ { @@ -794,36 +801,45 @@ "type": "github" } ], - "time": "2024-04-19T03:13:44+00:00" + "time": "2025-08-27T21:42:00+00:00" }, { - "name": "amphp/serialization", - "version": "v1.0.0", + "name": "amphp/byte-stream", + "version": "v2.1.2", "source": { "type": "git", - "url": "https://github.com/amphp/serialization.git", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", "shasum": "" }, "require": { - "php": ">=7.1" + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "phpunit/phpunit": "^9 || ^8 || ^7" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" }, "type": "library", "autoload": { "files": [ - "src/functions.php" + "src/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "Amp\\Serialization\\": "src" + "Amp\\ByteStream\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -840,61 +856,59 @@ "email": "me@kelunik.com" } ], - "description": "Serialization tools for IPC and data storage in PHP.", - "homepage": "https://github.com/amphp/serialization", + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", "keywords": [ + "amp", + "amphp", "async", - "asynchronous", - "serialization", - "serialize" + "io", + "non-blocking", + "stream" ], "support": { - "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/master" + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" }, - "time": "2020-03-25T21:39:07+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" }, { - "name": "amphp/socket", - "version": "v2.3.1", + "name": "amphp/cache", + "version": "v2.0.1", "source": { "type": "git", - "url": "https://github.com/amphp/socket.git", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", "shasum": "" }, "require": { "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/dns": "^2", - "ext-openssl": "*", - "kelunik/certificate": "^1.1", - "league/uri": "^6.5 | ^7", - "league/uri-interfaces": "^2.3 | ^7", + "amphp/serialization": "^1", + "amphp/sync": "^2", "php": ">=8.1", "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", - "amphp/process": "^2", "phpunit/phpunit": "^9", - "psalm/phar": "5.20" + "psalm/phar": "^5.4" }, "type": "library", "autoload": { - "files": [ - "src/functions.php", - "src/Internal/functions.php", - "src/SocketAddress/functions.php" - ], "psr-4": { - "Amp\\Socket\\": "src" + "Amp\\Cache\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -903,32 +917,23 @@ ], "authors": [ { - "name": "Daniel Lowrey", - "email": "rdlowrey@gmail.com" + "name": "Niklas Keller", + "email": "me@kelunik.com" }, { "name": "Aaron Piotrowski", "email": "aaron@trowski.com" }, { - "name": "Niklas Keller", - "email": "me@kelunik.com" + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], - "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", - "homepage": "https://github.com/amphp/socket", - "keywords": [ - "amp", - "async", - "encryption", - "non-blocking", - "sockets", - "tcp", - "tls" - ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", "support": { - "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.3.1" + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" }, "funding": [ { @@ -936,26 +941,31 @@ "type": "github" } ], - "time": "2024-04-21T14:33:03+00:00" + "time": "2024-04-19T03:38:06+00:00" }, { - "name": "amphp/sync", - "version": "v2.3.0", + "name": "amphp/dns", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/amphp/sync.git", - "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", - "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", "shasum": "" }, "require": { "amphp/amp": "^3", - "amphp/pipeline": "^1", - "amphp/serialization": "^1", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", "php": ">=8.1", "revolt/event-loop": "^1 || ^0.2" }, @@ -963,7 +973,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "5.23" + "psalm/phar": "5.20" }, "type": "library", "autoload": { @@ -971,7 +981,7 @@ "src/functions.php" ], "psr-4": { - "Amp\\Sync\\": "src" + "Amp\\Dns\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -980,30 +990,39 @@ ], "authors": [ { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" }, { "name": "Niklas Keller", "email": "me@kelunik.com" }, { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], - "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", - "homepage": "https://github.com/amphp/sync", + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", "keywords": [ + "amp", + "amphp", "async", - "asynchronous", - "mutex", - "semaphore", - "synchronization" + "client", + "dns", + "resolve" ], "support": { - "issues": "https://github.com/amphp/sync/issues", - "source": "https://github.com/amphp/sync/tree/v2.3.0" + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" }, "funding": [ { @@ -1011,47 +1030,51 @@ "type": "github" } ], - "time": "2024-08-03T19:31:26+00:00" + "time": "2025-01-19T15:43:40+00:00" }, { - "name": "composer/pcre", - "version": "3.3.2", + "name": "amphp/parallel", + "version": "v2.3.3", "source": { "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "url": "https://github.com/amphp/parallel.git", + "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", + "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" }, "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], "psr-4": { - "Composer\\Pcre\\": "src" + "Amp\\Parallel\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1060,68 +1083,65 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" ], "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.3" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2025-11-15T06:23:42+00:00" }, { - "name": "composer/semver", - "version": "3.4.4", + "name": "amphp/parser", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": ">=7.4" }, "require-dev": { - "phpstan/phpstan": "^1.11", - "symfony/phpunit-bridge": "^3 || ^7" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, "autoload": { "psr-4": { - "Composer\\Semver\\": "src" + "Amp\\Parser\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1130,73 +1150,63 @@ ], "authors": [ { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", "keywords": [ - "semantic", - "semver", - "validation", - "versioning" + "async", + "non-blocking", + "parser", + "stream" ], "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.4" + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2025-08-20T19:15:30+00:00" + "time": "2024-03-21T19:16:53+00:00" }, { - "name": "composer/xdebug-handler", - "version": "3.0.5", + "name": "amphp/pipeline", + "version": "v1.2.3", "source": { "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", "shasum": "" }, "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" }, "type": "library", "autoload": { "psr-4": { - "Composer\\XdebugHandler\\": "src" + "Amp\\Pipeline\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1205,108 +1215,124 @@ ], "authors": [ { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Restarts a process without Xdebug.", + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", "keywords": [ - "Xdebug", - "performance" + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" ], "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-05-06T16:37:16+00:00" + "time": "2025-03-16T16:33:53+00:00" }, { - "name": "danog/advanced-json-rpc", - "version": "v3.2.3", + "name": "amphp/process", + "version": "v2.0.3", "source": { "type": "git", - "url": "https://github.com/danog/php-advanced-json-rpc.git", - "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91" + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/ae703ea7b4811797a10590b6078de05b3b33dd91", - "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", "shasum": "" }, "require": { - "netresearch/jsonmapper": "^5", + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", "php": ">=8.1", - "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0 || ^6" - }, - "replace": { - "felixfbecker/php-advanced-json-rpc": "^3" + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "phpunit/phpunit": "^9" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "AdvancedJsonRpc\\": "lib/" + "Amp\\Process\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "ISC" + "MIT" ], "authors": [ { - "name": "Felix Becker", - "email": "felix.b@outlook.com" + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" }, { - "name": "Daniil Gentili", - "email": "daniil@daniil.it" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "A more advanced JSONRPC implementation", + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", "support": { - "issues": "https://github.com/danog/php-advanced-json-rpc/issues", - "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.3" + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" }, - "time": "2026-01-12T21:07:10+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" }, { - "name": "daverandom/libdns", - "version": "v2.1.0", + "name": "amphp/serialization", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/DaveRandom/LibDNS.git", - "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", - "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", "shasum": "" }, "require": { - "ext-ctype": "*", "php": ">=7.1" }, - "suggest": { - "ext-intl": "Required for IDN support" + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" }, "type": "library", "autoload": { @@ -1314,57 +1340,78 @@ "src/functions.php" ], "psr-4": { - "LibDNS\\": "src/" + "Amp\\Serialization\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "DNS protocol implementation written in pure PHP", + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", "keywords": [ - "dns" + "async", + "asynchronous", + "serialization", + "serialize" ], "support": { - "issues": "https://github.com/DaveRandom/LibDNS/issues", - "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" }, - "time": "2024-04-12T12:12:48+00:00" + "time": "2020-03-25T21:39:07+00:00" }, { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.2.0", + "name": "amphp/socket", + "version": "v2.3.1", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", "shasum": "" }, "require": { - "composer-plugin-api": "^2.2", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "composer/composer": "^2.2", - "ext-json": "*", - "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", - "yoast/phpunit-polyfills": "^1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" }, + "type": "library", "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], "psr-4": { - "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "Amp\\Socket\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1373,233 +1420,300 @@ ], "authors": [ { - "name": "Franck Nijhof", - "email": "opensource@frenck.dev", - "homepage": "https://frenck.dev", - "role": "Open source developer" + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcbf", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" ], "support": { - "issues": "https://github.com/PHPCSStandards/composer-installer/issues", - "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", - "source": "https://github.com/PHPCSStandards/composer-installer" + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" }, "funding": [ { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" } ], - "time": "2025-11-11T04:32:07+00:00" + "time": "2024-04-21T14:33:03+00:00" }, { - "name": "dnoegel/php-xdg-base-dir", - "version": "v0.1.1", + "name": "amphp/sync", + "version": "v2.3.0", "source": { "type": "git", - "url": "https://github.com/dnoegel/php-xdg-base-dir.git", - "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", - "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", "shasum": "" }, "require": { - "php": ">=5.3.2" + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" }, "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "XdgBaseDir\\": "src/" + "Amp\\Sync\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "implementation of xdg base directory specification for php", + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], "support": { - "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", - "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" }, - "time": "2019-12-04T15:06:13+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" }, { - "name": "doctrine/deprecations", - "version": "1.1.6", + "name": "composer/pcre", + "version": "3.3.2", "source": { "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.4 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=14" + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^14", - "phpstan/phpstan": "1.4.10 || 2.1.30", - "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", - "psr/log": "^1 || ^2 || ^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" }, "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { "psr-4": { - "Doctrine\\Deprecations\\": "src" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" }, - "time": "2026-02-07T07:09:04+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" }, { - "name": "felixfbecker/language-server-protocol", - "version": "v1.5.3", + "name": "composer/semver", + "version": "3.4.4", "source": { "type": "git", - "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", - "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { - "php": ">=7.1" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "*", - "squizlabs/php_codesniffer": "^3.1", - "vimeo/psalm": "^4.0" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "LanguageServerProtocol\\": "src/" + "Composer\\Semver\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "ISC" + "MIT" ], "authors": [ { - "name": "Felix Becker", - "email": "felix.b@outlook.com" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" } ], - "description": "PHP classes for the Language Server Protocol", + "description": "Semver library that offers utilities, version constraint parsing and validation.", "keywords": [ - "language", - "microsoft", - "php", - "server" + "semantic", + "semver", + "validation", + "versioning" ], "support": { - "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" }, - "time": "2024-04-30T00:40:11+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" }, { - "name": "fidry/cpu-core-counter", - "version": "1.3.0", + "name": "composer/xdebug-handler", + "version": "3.0.5", "source": { "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", - "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "fidry/makefile": "^0.2.0", - "fidry/php-cs-fixer-config": "^1.1.2", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-deprecation-rules": "^2.0.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^8.5.31 || ^9.5.26", - "webmozarts/strict-phpunit": "^7.5" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" + "Composer\\XdebugHandler\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1608,597 +1722,532 @@ ], "authors": [ { - "name": "Thรฉo FIDRY", - "email": "theo.fidry@gmail.com" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Tiny utility to get the number of CPU cores.", + "description": "Restarts a process without Xdebug.", "keywords": [ - "CPU", - "core" + "Xdebug", + "performance" ], "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { - "url": "https://github.com/theofidry", + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2025-08-14T07:29:31+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { - "name": "kelunik/certificate", - "version": "v1.1.3", + "name": "danog/advanced-json-rpc", + "version": "v3.2.3", "source": { "type": "git", - "url": "https://github.com/kelunik/certificate.git", - "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + "url": "https://github.com/danog/php-advanced-json-rpc.git", + "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", - "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/ae703ea7b4811797a10590b6078de05b3b33dd91", + "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91", "shasum": "" }, "require": { - "ext-openssl": "*", - "php": ">=7.0" + "netresearch/jsonmapper": "^5", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0 || ^6" + }, + "replace": { + "felixfbecker/php-advanced-json-rpc": "^3" }, "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + "phpunit/phpunit": "^9" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { "psr-4": { - "Kelunik\\Certificate\\": "src" + "AdvancedJsonRpc\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "ISC" ], "authors": [ { - "name": "Niklas Keller", - "email": "me@kelunik.com" + "name": "Felix Becker", + "email": "felix.b@outlook.com" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" } ], - "description": "Access certificate details and transform between different formats.", - "keywords": [ - "DER", - "certificate", - "certificates", - "openssl", - "pem", - "x509" - ], + "description": "A more advanced JSONRPC implementation", "support": { - "issues": "https://github.com/kelunik/certificate/issues", - "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + "issues": "https://github.com/danog/php-advanced-json-rpc/issues", + "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.3" }, - "time": "2023-02-03T21:26:53+00:00" + "time": "2026-01-12T21:07:10+00:00" }, { - "name": "laminas/laminas-coding-standard", - "version": "3.1.0", + "name": "daverandom/libdns", + "version": "v2.1.0", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-coding-standard.git", - "reference": "d4412caba9ed16c93cdcf301759f5ee71f9d9aea" + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-coding-standard/zipball/d4412caba9ed16c93cdcf301759f5ee71f9d9aea", - "reference": "d4412caba9ed16c93cdcf301759f5ee71f9d9aea", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "php": "^7.4 || ^8.0", - "slevomat/coding-standard": "^8.15.0", - "squizlabs/php_codesniffer": "^3.10", - "webimpress/coding-standard": "^1.3" + "ext-ctype": "*", + "php": ">=7.1" }, - "type": "phpcodesniffer-standard", + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "LaminasCodingStandard\\": "src/LaminasCodingStandard/" + "LibDNS\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "Laminas Coding Standard", - "homepage": "https://laminas.dev", + "description": "DNS protocol implementation written in pure PHP", "keywords": [ - "Coding Standard", - "laminas" + "dns" ], "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-coding-standard/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-coding-standard/issues", - "rss": "https://github.com/laminas/laminas-coding-standard/releases.atom", - "source": "https://github.com/laminas/laminas-coding-standard" + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2025-05-13T08:37:04+00:00" + "time": "2024-04-12T12:12:48+00:00" }, { - "name": "laminas/laminas-escaper", - "version": "2.18.0", + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-escaper.git", - "reference": "06f211dfffff18d91844c1f55250d5d13c007e18" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/06f211dfffff18d91844c1f55250d5d13c007e18", - "reference": "06f211dfffff18d91844c1f55250d5d13c007e18", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-mbstring": "*", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" - }, - "conflict": { - "zendframework/zend-escaper": "*" + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" }, "require-dev": { - "infection/infection": "^0.31.0", - "laminas/laminas-coding-standard": "~3.1.0", - "phpunit/phpunit": "^11.5.42", - "psalm/plugin-phpunit": "^0.19.5", - "vimeo/psalm": "^6.13.1" + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, - "type": "library", "autoload": { "psr-4": { - "Laminas\\Escaper\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" ], - "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", - "homepage": "https://laminas.dev", + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", "keywords": [ - "escaper", - "laminas" + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" ], "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-escaper/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-escaper/issues", - "rss": "https://github.com/laminas/laminas-escaper/releases.atom", - "source": "https://github.com/laminas/laminas-escaper" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" }, "funding": [ { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2025-10-14T18:31:13+00:00" + "time": "2025-11-11T04:32:07+00:00" }, { - "name": "laminas/laminas-http", - "version": "2.23.0", + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-http.git", - "reference": "9462fc84330d25b23383823831380abb33907fdd" + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-http/zipball/9462fc84330d25b23383823831380abb33907fdd", - "reference": "9462fc84330d25b23383823831380abb33907fdd", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", "shasum": "" }, "require": { - "laminas/laminas-loader": "^2.10", - "laminas/laminas-stdlib": "^3.6", - "laminas/laminas-uri": "^2.14", - "laminas/laminas-validator": "^2.15 || ^3.0", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" - }, - "conflict": { - "zendframework/zend-http": "*" + "php": ">=5.3.2" }, "require-dev": { - "ext-curl": "*", - "laminas/laminas-coding-standard": "^3.0.1", - "phpunit/phpunit": "^10.5.38" - }, - "suggest": { - "paragonie/certainty": "For automated management of cacert.pem" + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" }, "type": "library", "autoload": { "psr-4": { - "Laminas\\Http\\": "src/" + "XdgBaseDir\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "description": "Provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests", - "homepage": "https://laminas.dev", - "keywords": [ - "http", - "http client", - "laminas" + "MIT" ], + "description": "implementation of xdg base directory specification for php", "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-http/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-http/issues", - "rss": "https://github.com/laminas/laminas-http/releases.atom", - "source": "https://github.com/laminas/laminas-http" + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2025-12-05T11:02:08+00:00" + "time": "2019-12-04T15:06:13+00:00" }, { - "name": "laminas/laminas-i18n", - "version": "2.32.1", + "name": "doctrine/deprecations", + "version": "1.1.5", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-i18n.git", - "reference": "8e71b40318f0df6253329e837188e1d77cf83aea" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/8e71b40318f0df6253329e837188e1d77cf83aea", - "reference": "8e71b40318f0df6253329e837188e1d77cf83aea", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-intl": "*", - "laminas/laminas-escaper": "^2.0", - "laminas/laminas-servicemanager": "^3.21.0", - "laminas/laminas-stdlib": "^3.0", - "laminas/laminas-translator": "^1.0", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "psr/container": "^1.0.0" + "php": "^7.1 || ^8.0" }, "conflict": { - "laminas/laminas-view": "<2.20.0", - "zendframework/zend-i18n": "*" + "phpunit/phpunit": "<=7.5 || >=13" }, "require-dev": { - "laminas/laminas-cache": "^3.13.0", - "laminas/laminas-cache-storage-adapter-memory": "^2.4.0", - "laminas/laminas-cache-storage-deprecated-factory": "^1.3", - "laminas/laminas-coding-standard": "^3.1", - "laminas/laminas-config": "^3.10.1", - "laminas/laminas-eventmanager": "^3.15.0", - "laminas/laminas-filter": "^2.42", - "laminas/laminas-validator": "^2.65.0", - "laminas/laminas-view": "^2.44", - "phpunit/phpunit": "^11.5.46", - "psalm/plugin-phpunit": "^0.19.5", - "vimeo/psalm": "^6.14.2" + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" }, "suggest": { - "laminas/laminas-cache": "You should install this package to cache the translations", - "laminas/laminas-config": "You should install this package to use the INI translation format", - "laminas/laminas-eventmanager": "You should install this package to use the events in the translator", - "laminas/laminas-filter": "You should install this package to use the provided filters", - "laminas/laminas-i18n-resources": "This package provides validator and captcha translations", - "laminas/laminas-validator": "You should install this package to use the provided validators", - "laminas/laminas-view": "You should install this package to use the provided view helpers" + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" }, "type": "library", - "extra": { - "laminas": { - "component": "Laminas\\I18n", - "config-provider": "Laminas\\I18n\\ConfigProvider" - } - }, "autoload": { "psr-4": { - "Laminas\\I18n\\": "src/" + "Doctrine\\Deprecations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "description": "Provide translations for your application, and filter and validate internationalized values", - "homepage": "https://laminas.dev", - "keywords": [ - "i18n", - "laminas" + "MIT" ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-i18n/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-i18n/issues", - "rss": "https://github.com/laminas/laminas-i18n/releases.atom", - "source": "https://github.com/laminas/laminas-i18n" + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2025-12-15T14:23:40+00:00" + "time": "2025-04-07T20:06:18+00:00" }, { - "name": "laminas/laminas-loader", - "version": "2.12.0", + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.3", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-loader.git", - "reference": "ec8cee33fb254ee4d9c8e8908c870e5c797e1272" + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-loader/zipball/ec8cee33fb254ee4d9c8e8908c870e5c797e1272", - "reference": "ec8cee33fb254ee4d9c8e8908c870e5c797e1272", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", "shasum": "" }, "require": { - "php": "^8.0.0" - }, - "conflict": { - "zendframework/zend-loader": "*" + "php": ">=7.1" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.4.0", - "phpunit/phpunit": "~9.5.25" + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { "psr-4": { - "Laminas\\Loader\\": "src/" + "LanguageServerProtocol\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "ISC" ], - "description": "Autoloading and plugin loading strategies", - "homepage": "https://laminas.dev", + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", "keywords": [ - "laminas", - "loader" + "language", + "microsoft", + "php", + "server" ], "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-loader/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-loader/issues", - "rss": "https://github.com/laminas/laminas-loader/releases.atom", - "source": "https://github.com/laminas/laminas-loader" + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "abandoned": true, - "time": "2025-12-30T11:30:39+00:00" + "time": "2024-04-30T00:40:11+00:00" }, { - "name": "laminas/laminas-translator", + "name": "fidry/cpu-core-counter", "version": "1.3.0", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-translator.git", - "reference": "1d6bf7d857c3fc310a30f5ea028731b9bf8be9db" + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-translator/zipball/1d6bf7d857c3fc310a30f5ea028731b9bf8be9db", - "reference": "1d6bf7d857c3fc310a30f5ea028731b9bf8be9db", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "laminas/laminas-coding-standard": "~3.1.0", - "phpunit/phpunit": "^11.5.46", - "vimeo/psalm": "^6.14.3" + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" }, "type": "library", "autoload": { "psr-4": { - "Laminas\\Translator\\": "src/" + "Fidry\\CpuCoreCounter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "Interfaces for the Translator component of laminas-i18n", - "homepage": "https://laminas.dev", + "authors": [ + { + "name": "Thรฉo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", "keywords": [ - "i18n", - "laminas" + "CPU", + "core" ], "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" + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" + "url": "https://github.com/theofidry", + "type": "github" } ], - "time": "2025-12-30T14:26:45+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { - "name": "laminas/laminas-uri", - "version": "2.14.0", + "name": "kelunik/certificate", + "version": "v1.1.3", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-uri.git", - "reference": "e804288f4540988903dc0ede386ce5eec87198df" + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-uri/zipball/e804288f4540988903dc0ede386ce5eec87198df", - "reference": "e804288f4540988903dc0ede386ce5eec87198df", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", "shasum": "" }, "require": { - "laminas/laminas-escaper": "^2.9", - "laminas/laminas-validator": "^2.39 || ^3.0", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" - }, - "conflict": { - "zendframework/zend-uri": "*" + "ext-openssl": "*", + "php": ">=7.0" }, "require-dev": { - "laminas/laminas-coding-standard": "~2.4.0", - "phpunit/phpunit": "^11.0" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } }, - "type": "library", "autoload": { "psr-4": { - "Laminas\\Uri\\": "src/" + "Kelunik\\Certificate\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "A component that aids in manipulating and validating ยป Uniform Resource Identifiers (URIs)", - "homepage": "https://laminas.dev", + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", "keywords": [ - "laminas", - "uri" + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" ], "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-uri/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-uri/issues", - "rss": "https://github.com/laminas/laminas-uri/releases.atom", - "source": "https://github.com/laminas/laminas-uri" + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2025-12-05T10:02:11+00:00" + "time": "2023-02-03T21:26:53+00:00" }, { - "name": "laminas/laminas-validator", - "version": "2.65.0", + "name": "laminas/laminas-coding-standard", + "version": "3.1.0", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-validator.git", - "reference": "f0767ca83e0dd91a6f8ccdd4f0887eb132c0ea49" + "url": "https://github.com/laminas/laminas-coding-standard.git", + "reference": "d4412caba9ed16c93cdcf301759f5ee71f9d9aea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/f0767ca83e0dd91a6f8ccdd4f0887eb132c0ea49", - "reference": "f0767ca83e0dd91a6f8ccdd4f0887eb132c0ea49", + "url": "https://api.github.com/repos/laminas/laminas-coding-standard/zipball/d4412caba9ed16c93cdcf301759f5ee71f9d9aea", + "reference": "d4412caba9ed16c93cdcf301759f5ee71f9d9aea", "shasum": "" }, "require": { - "laminas/laminas-servicemanager": "^3.21.0", - "laminas/laminas-stdlib": "^3.19", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "psr/http-message": "^1.0.1 || ^2.0.0" - }, - "conflict": { - "zendframework/zend-validator": "*" - }, - "require-dev": { - "laminas/laminas-coding-standard": "^2.5", - "laminas/laminas-db": "^2.20", - "laminas/laminas-filter": "^2.41.0", - "laminas/laminas-i18n": "^2.30.0", - "laminas/laminas-session": "^2.25.1", - "laminas/laminas-uri": "^2.13.0", - "phpunit/phpunit": "^10.5.58", - "psalm/plugin-phpunit": "^0.19.0", - "psr/http-client": "^1.0.3", - "psr/http-factory": "^1.1.0", - "vimeo/psalm": "^5.26.1" - }, - "suggest": { - "laminas/laminas-db": "Laminas\\Db component, required by the (No)RecordExists validator", - "laminas/laminas-filter": "Laminas\\Filter component, required by the Digits validator", - "laminas/laminas-i18n": "Laminas\\I18n component to allow translation of validation error messages", - "laminas/laminas-i18n-resources": "Translations of validator messages", - "laminas/laminas-servicemanager": "Laminas\\ServiceManager component to allow using the ValidatorPluginManager and validator chains", - "laminas/laminas-session": "Laminas\\Session component, ^2.8; required by the Csrf validator", - "laminas/laminas-uri": "Laminas\\Uri component, required by the Uri and Sitemap\\Loc validators", - "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators" - }, - "type": "library", - "extra": { - "laminas": { - "component": "Laminas\\Validator", - "config-provider": "Laminas\\Validator\\ConfigProvider" - } + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "php": "^7.4 || ^8.0", + "slevomat/coding-standard": "^8.15.0", + "squizlabs/php_codesniffer": "^3.10", + "webimpress/coding-standard": "^1.3" }, + "type": "phpcodesniffer-standard", "autoload": { "psr-4": { - "Laminas\\Validator\\": "src/" + "LaminasCodingStandard\\": "src/LaminasCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "description": "Validation classes for a wide range of domains, and the ability to chain validators to create complex validation criteria", + "description": "Laminas Coding Standard", "homepage": "https://laminas.dev", "keywords": [ - "laminas", - "validator" + "Coding Standard", + "laminas" ], "support": { "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-validator/", + "docs": "https://docs.laminas.dev/laminas-coding-standard/", "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-validator/issues", - "rss": "https://github.com/laminas/laminas-validator/releases.atom", - "source": "https://github.com/laminas/laminas-validator" + "issues": "https://github.com/laminas/laminas-coding-standard/issues", + "rss": "https://github.com/laminas/laminas-coding-standard/releases.atom", + "source": "https://github.com/laminas/laminas-coding-standard" }, "funding": [ { @@ -2206,7 +2255,7 @@ "type": "community_bridge" } ], - "time": "2025-10-13T14:40:30+00:00" + "time": "2025-05-13T08:37:04+00:00" }, { "name": "league/uri", @@ -2501,64 +2550,6 @@ }, "time": "2024-09-08T10:20:00+00:00" }, - { - "name": "nikic/php-parser", - "version": "v5.7.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" - }, - "time": "2025-12-06T11:56:16+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -3249,16 +3240,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.52", + "version": "11.5.50", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b287d32c26f78768e391843c5a59395f24b62605" + "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b287d32c26f78768e391843c5a59395f24b62605", - "reference": "b287d32c26f78768e391843c5a59395f24b62605", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", "shasum": "" }, "require": { @@ -3273,7 +3264,7 @@ "phar-io/version": "^3.2.1", "php": ">=8.2", "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", @@ -3285,7 +3276,6 @@ "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/recursion-context": "^6.0.3", "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" @@ -3331,7 +3321,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.52" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50" }, "funding": [ { @@ -3355,7 +3345,7 @@ "type": "tidelift" } ], - "time": "2026-02-08T07:05:14+00:00" + "time": "2026-01-27T05:59:18+00:00" }, { "name": "psalm/plugin-phpunit", @@ -3415,114 +3405,6 @@ }, "time": "2025-03-31T18:49:55+00:00" }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, { "name": "psr/log", "version": "3.0.2", @@ -5775,16 +5657,16 @@ }, { "name": "vimeo/psalm", - "version": "6.15.1", + "version": "6.15.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "28dc127af1b5aecd52314f6f645bafc10d0e11f9" + "reference": "204d06619833a297b402cbc66be7f2df74437db4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/28dc127af1b5aecd52314f6f645bafc10d0e11f9", - "reference": "28dc127af1b5aecd52314f6f645bafc10d0e11f9", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/204d06619833a297b402cbc66be7f2df74437db4", + "reference": "204d06619833a297b402cbc66be7f2df74437db4", "shasum": "" }, "require": { @@ -5808,7 +5690,7 @@ "netresearch/jsonmapper": "^5.0", "nikic/php-parser": "^5.0.0", "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", "symfony/console": "^6.0 || ^7.0 || ^8.0", "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3 || ^8.0", @@ -5889,7 +5771,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2026-02-07T19:27:16+00:00" + "time": "2026-01-30T13:53:17+00:00" }, { "name": "webimpress/coding-standard", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 450dd7a9..75f06f6d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,72 +1,30 @@ - + + + + + + - - - - - - - - getPath()]]> - - - - - - assemble($params, $chainOptions)]]> - - - - - - - - - - - chainRoutes !== null]]> - chainRoutes !== null]]> - - - - - - - - - - - - - - - - assembledParams]]> - + + + + defaults, $params)]]> @@ -74,6 +32,24 @@ + + + + + + + + + + + + + + + + + + @@ -81,253 +57,107 @@ + + + + + + defaults[$part[1]]]]> + defaults[$part[1]]]]> + + + + + + assembledParams]]> + - - - - - - - - defaults[$part[1]]]]> - - - - - - - - - - - - - - - - - - - - - createRouter($class, $config, $container)]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getMethod()]]> - - - - - - - - childRoutes]]> - childRoutes]]> - getPath()]]> + - + - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - matchedRouteName === null]]> - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -336,9 +166,6 @@ - - - @@ -377,30 +204,21 @@ - defaults[$part[1]]]]> defaults[$part[1]]]]> - - - - - - - - @@ -417,250 +235,92 @@ - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - translator !== null]]> - - - - - - - - - [ - 'chain' => Chain::class, - 'Chain' => Chain::class, - 'hostname' => Hostname::class, - 'Hostname' => Hostname::class, - 'hostName' => Hostname::class, - 'HostName' => Hostname::class, - 'literal' => Literal::class, - 'Literal' => Literal::class, - 'method' => Method::class, - 'Method' => Method::class, - 'part' => Part::class, - 'Part' => Part::class, - 'regex' => Regex::class, - 'Regex' => Regex::class, - 'scheme' => Scheme::class, - 'Scheme' => Scheme::class, - 'segment' => Segment::class, - 'Segment' => Segment::class, - 'wildcard' => Wildcard::class, - 'Wildcard' => Wildcard::class, - 'wildCard' => Wildcard::class, - 'WildCard' => Wildcard::class, - ], - 'factories' => [ - Chain::class => RouteInvokableFactory::class, - Hostname::class => RouteInvokableFactory::class, - Literal::class => RouteInvokableFactory::class, - Method::class => RouteInvokableFactory::class, - Part::class => RouteInvokableFactory::class, - Regex::class => RouteInvokableFactory::class, - Scheme::class => RouteInvokableFactory::class, - Segment::class => RouteInvokableFactory::class, - Wildcard::class => RouteInvokableFactory::class, - - // v2 normalized names - 'laminasmvcrouterhttpchain' => RouteInvokableFactory::class, - 'laminasmvcrouterhttphostname' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpliteral' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpmethod' => RouteInvokableFactory::class, - 'laminasmvcrouterhttppart' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpregex' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpscheme' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpsegment' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpwildcard' => RouteInvokableFactory::class, - ], - ])]]> - - - - baseUrl === null]]> - requestUri === null]]> - requestUri === null]]> - requestUri === null]]> - getHost() === null || $uri->getScheme() === null) && $this->requestUri === null]]> - - - + + + - + + getBaseUrl()]]> - - - + - - - - - - - - assemble(array_merge($this->defaultParams, $params), $options)]]> assemble(array_merge($this->defaultParams, $params), $options)]]> - - - - - - - - - priority]]> - + + + - - - - - - - - + + - - - - - - - - - - - - - - - baseUrl]]> - - - - - - - - - - - keyValueDelimiter]]> - paramDelimiter]]> - - - - - - - - - - - - - - - - - - - - - - + - - - - + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + @@ -668,7 +328,6 @@ - @@ -678,54 +337,22 @@ - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - priority]]> - - - - - - - - @@ -736,43 +363,26 @@ + - - - - - - - - + + + + - - - 'bat']]> - - - - - - - - - - - @@ -787,139 +397,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [ - self::getRouteAlternative(), - '/', - null, - null, - [ - 'controller' => 'fo-fo', - 'action' => 'index', - ], - ]]]> - + + + [ - 'literal' => Literal::class, - 'Literal' => Literal::class, - 'part' => Part::class, - 'Part' => Part::class, - 'segment' => Segment::class, - 'Segment' => Segment::class, - 'wildcard' => Wildcard::class, - 'Wildcard' => Wildcard::class, - 'wildCard' => Wildcard::class, - 'WildCard' => Wildcard::class, + 'literal' => Literal::class, + 'Literal' => Literal::class, + 'part' => Part::class, + 'Part' => Part::class, + 'segment' => Segment::class, + 'Segment' => Segment::class, ], 'factories' => [ - Literal::class => RouteInvokableFactory::class, - Part::class => RouteInvokableFactory::class, - Segment::class => RouteInvokableFactory::class, - Wildcard::class => RouteInvokableFactory::class, - - // v2 normalized names - 'laminasmvcrouterhttpliteral' => RouteInvokableFactory::class, - 'laminasmvcrouterhttppart' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpsegment' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpwildcard' => RouteInvokableFactory::class, + Literal::class => RouteInvokableFactory::class, + Part::class => RouteInvokableFactory::class, + Segment::class => RouteInvokableFactory::class, ], ])]]> - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -938,253 +460,100 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + translator]]> + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - get('foo')->priority]]> - - - - - - - - - - - - * }>]]> - - - - 'baz'])]]> - - - - - - - - - - - - - - [ - new Wildcard(), - '/foo/bar/baz/bat', - null, - ['foo' => 'bar', 'baz' => 'bat'], - ], - 'empty-match' => [ - new Wildcard(), - '', - null, - [], - ], - 'no-match-without-leading-delimiter' => [ - new Wildcard(), - '/foo/foo/bar/baz/bat', - 5, - null, - ], - 'no-match-with-trailing-slash' => [ - new Wildcard(), - '/foo/bar/baz/bat/', - null, - null, - ], - 'match-overrides-default' => [ - new Wildcard('/', '/', ['foo' => 'baz']), - '/foo/bat', - null, - ['foo' => 'bat'], - ], - 'offset-skips-beginning' => [ - new Wildcard(), - '/bat/foo/bar', - 4, - ['foo' => 'bar'], - ], - 'non-standard-key-value-delimiter' => [ - new Wildcard('-'), - '/foo-bar/baz-bat', - null, - ['foo' => 'bar', 'baz' => 'bat'], - ], - 'non-standard-parameter-delimiter' => [ - new Wildcard('/', '-'), - '/foo/-foo/bar-baz/bat', - 5, - ['foo' => 'bar', 'baz' => 'bat'], - ], - 'empty-values-with-non-standard-key-value-delimiter-are-omitted' => [ - new Wildcard('-'), - '/foo', - null, - [], - true, - ], - 'url-encoded-parameters-are-decoded' => [ - new Wildcard(), - '/foo/foo%20bar', - null, - ['foo' => 'foo bar'], - ], - 'params-contain-non-string-scalar-values' => [ - new Wildcard(), - '/int_param/42/float_param/4.2', - null, - ['int_param' => 42, 'float_param' => 4.2], - ], - ]]]> - - - - - - - - - - - - - - - - - - - - - - * }>]]> - - - - - - - - - - + @@ -1192,9 +561,6 @@ - - - @@ -1217,10 +583,6 @@ - - container]]> - container]]> - @@ -1232,26 +594,6 @@ - - defaultServiceConfig, [ - 'services' => [ - 'config' => [ - 'router' => [ - 'router_class' => TestAsset\Router::class, - ], - ], - ], - ]))]]> - defaultServiceConfig, [ - 'services' => [ - 'config' => [ - 'router' => [ - 'router_class' => TestAsset\Router::class, - ], - ], - ], - ]))]]> - @@ -1261,49 +603,15 @@ + + + + - - priority]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - priority]]> - @@ -1312,22 +620,34 @@ - - - + + + + + + + + + + + headers]]> + headers]]> + + - - - + - - - + + + + + + diff --git a/psalm.xml.dist b/psalm.xml.dist index 09567571..404f7d90 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -26,11 +26,6 @@ - - - - - diff --git a/src/AbstractRouteStack.php b/src/AbstractRouteStack.php new file mode 100644 index 00000000..36f8b9cb --- /dev/null +++ b/src/AbstractRouteStack.php @@ -0,0 +1,180 @@ + + */ + protected array $defaultParams = []; + + protected RoutePluginManager $routePluginManager; + + /** @var TContainer */ + protected RouteContainerInterface $routes; + + /** + * Init method for extending classes. + */ + protected function init(): void + { + } + + /** + * @param RoutePluginManager $routePlugins + * @return $this + */ + public function setRoutePluginManager(RoutePluginManager $routePlugins): static + { + $this->routePluginManager = $routePlugins; + + return $this; + } + + /** + * Get the route plugin manager. + */ + public function getRoutePluginManager(): RoutePluginManager + { + return $this->routePluginManager; + } + + /** + * @inheritDoc + * @throws ContainerExceptionInterface + */ + public function addRoutes(iterable $routes): RouteStackInterface + { + foreach ($routes as $name => $route) { + $this->addRoute((string) $name, $route); + } + + return $this; + } + + /** + * @inheritDoc + * @throws ContainerExceptionInterface + */ + public function addRoute( + string $name, + iterable|RouteInterface $route, + ?int $priority = null + ): RouteStackInterface { + if (! $route instanceof RouteInterface) { + $route = $this->routeFromIterable($route); + } + + $this->routes->insert($name, $route, $priority ?? $route->getPriority()); + + return $this; + } + + /** @inheritDoc */ + public function removeRoute(string $name): RouteStackInterface + { + $this->routes->remove($name); + + return $this; + } + + /** + * @inheritDoc + * @throws ContainerExceptionInterface + */ + public function setRoutes(iterable $routes): RouteStackInterface + { + $this->routes->clear(); + $this->addRoutes($routes); + + return $this; + } + + /** + * Get the added routes. + * + * @return TContainer + */ + public function getRoutes(): RouteContainerInterface + { + return $this->routes; + } + + /** + * Check if a route with a specific name exists. + */ + public function hasRoute(string $name): bool + { + return $this->routes->get($name) !== null; + } + + /** + * Get a route by name. + */ + public function getRoute(string $name): ?RouteInterface + { + return $this->routes->get($name); + } + + /** + * Set default parameters. + * + * @param array $params + */ + public function setDefaultParams(array $params): static + { + $this->defaultParams = $params; + + return $this; + } + + /** + * Set a default parameter. + */ + public function setDefaultParam(string $name, mixed $value): static + { + $this->defaultParams[$name] = $value; + + return $this; + } + + /** + * Create a route from array specifications. + * + * @throws ContainerExceptionInterface + */ + protected function routeFromIterable(iterable $specs): RouteInterface + { + $specs = self::processRouteOptions( + $specs, + ['type'], + ['options' => []], + ); + $type = (string) $specs['type']; + + $routePluginManager = $this->getRoutePluginManager(); + /** @psalm-var RouteInterface $route */ + $route = $routePluginManager->build($type, (array) $specs['options']); + + $priority = isset($specs['priority']) ? (int) $specs['priority'] : null; + $route->setPriority($priority); + + return $route; + } +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index d84e4937..9b2c1034 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -4,7 +4,7 @@ namespace Laminas\Router; -use Laminas\ServiceManager\ConfigInterface; +use Laminas\ServiceManager\ServiceManager; /** * Provide base configuration for using the component. @@ -16,16 +16,20 @@ * * @see ConfigInterface * - * @psalm-import-type ServiceManagerConfigurationType from ConfigInterface + * @psalm-import-type ServiceManagerConfiguration from ServiceManager + * @psalm-type RouterConfigShape = array{ + * dependencies: ServiceManagerConfiguration, + * route_manager: array + * } */ -class ConfigProvider +final readonly class ConfigProvider { /** * Provide default configuration. * - * @return array + * @return RouterConfigShape */ - public function __invoke() + public function __invoke(): array { return [ 'dependencies' => $this->getDependencyConfig(), @@ -36,7 +40,7 @@ public function __invoke() /** * Provide default container dependency configuration. * - * @return ServiceManagerConfigurationType + * @return ServiceManagerConfiguration */ public function getDependencyConfig() { @@ -46,11 +50,6 @@ public function getDependencyConfig() 'router' => RouteStackInterface::class, 'Router' => RouteStackInterface::class, 'RoutePluginManager' => RoutePluginManager::class, - - // Legacy Zend Framework aliases - 'Zend\Router\Http\TreeRouteStack' => Http\TreeRouteStack::class, - 'Zend\Router\RoutePluginManager' => RoutePluginManager::class, - 'Zend\Router\RouteStackInterface' => RouteStackInterface::class, ], 'factories' => [ Http\TreeRouteStack::class => Http\HttpRouterFactory::class, @@ -62,10 +61,8 @@ public function getDependencyConfig() /** * Provide default route plugin manager configuration. - * - * @return array */ - public function getRouteManagerConfig() + public function getRouteManagerConfig(): array { return []; } diff --git a/src/Container/AbstractRouteContainer.php b/src/Container/AbstractRouteContainer.php new file mode 100644 index 00000000..7a2bca43 --- /dev/null +++ b/src/Container/AbstractRouteContainer.php @@ -0,0 +1,153 @@ + + */ +abstract class AbstractRouteContainer implements RouteContainerInterface +{ + /** @var array */ + protected array $items = []; + + /** @var list Sorted keys for iteration */ + protected array $sortedKeys = []; + + /** @var int Increments for LIFO ordering within same priority */ + protected int $serial = 0; + + protected int $position = 0; + + protected bool $sorted = false; + + /** + * @param TValue $value + */ + abstract public function insert(string $key, RouteInterface $value, ?int $priority = null): void; + + /** + * @return TValue|null + */ + abstract public function get(string $key): ?RouteInterface; + + /** + * @return TValue + */ + abstract public function current(): RouteInterface; + + /** + * Store an item in the container. + * + * @param TValue $value + */ + protected function setRoute(string $key, RouteInterface $value, ?int $priority): void + { + $this->items[$key] = [ + 'value' => $value, + 'priority' => $priority ?? 0, + 'serial' => $this->serial++, + ]; + $this->sorted = false; + } + + /** + * Retrieve an item from the container. + * + * @return TValue|null + */ + protected function getRoute(string $key): ?RouteInterface + { + return $this->items[$key]['value'] ?? null; + } + + /** + * Get the current item during iteration. + * + * @return TValue + */ + protected function getCurrentRoute(): RouteInterface + { + $this->sort(); + return $this->items[$this->sortedKeys[$this->position]]['value']; + } + + public function remove(string $key): void + { + unset($this->items[$key]); + $this->sorted = false; + } + + public function clear(): void + { + $this->items = []; + $this->sortedKeys = []; + $this->sorted = true; + $this->position = 0; + } + + /** @psalm-suppress PossiblyUnusedMethod Required by Countable interface */ + public function count(): int + { + return count($this->items); + } + + /** + * Sort items by priority (higher first) and serial (higher first for LIFO). + */ + protected function sort(): void + { + if ($this->sorted) { + return; + } + + // Cast keys to string explicitly - PHP converts numeric string keys to int + $this->sortedKeys = array_map('strval', array_keys($this->items)); + usort($this->sortedKeys, function (string $a, string $b): int { + $itemA = $this->items[$a]; + $itemB = $this->items[$b]; + // Higher priority first, then higher serial (LIFO) + return $itemB['priority'] <=> $itemA['priority'] + ?: $itemB['serial'] <=> $itemA['serial']; + }); + $this->sorted = true; + $this->position = 0; + } + + public function key(): ?string + { + $this->sort(); + return $this->sortedKeys[$this->position] ?? null; + } + + public function next(): void + { + $this->position++; + } + + public function rewind(): void + { + $this->sort(); + $this->position = 0; + } + + public function valid(): bool + { + $this->sort(); + return isset($this->sortedKeys[$this->position]); + } +} diff --git a/src/Container/HttpRouteContainer.php b/src/Container/HttpRouteContainer.php new file mode 100644 index 00000000..705ed77e --- /dev/null +++ b/src/Container/HttpRouteContainer.php @@ -0,0 +1,31 @@ + + */ +final class HttpRouteContainer extends AbstractRouteContainer implements HttpRouteContainerInterface +{ + public function insert(string $key, BaseRouteInterface $value, ?int $priority = null): void + { + $this->setRoute($key, $value, $priority); + } + + public function get(string $key): ?HttpRouteInterface + { + return $this->getRoute($key); + } + + public function current(): HttpRouteInterface + { + return $this->getCurrentRoute(); + } +} diff --git a/src/Container/HttpRouteContainerInterface.php b/src/Container/HttpRouteContainerInterface.php new file mode 100644 index 00000000..86c05d4a --- /dev/null +++ b/src/Container/HttpRouteContainerInterface.php @@ -0,0 +1,31 @@ + + */ +interface HttpRouteContainerInterface extends RouteContainerInterface +{ + /** + * Insert a route with a given key and priority. + */ + public function insert(string $key, BaseRouteInterface $value, ?int $priority = null): void; + + /** + * Get a route by key. + */ + public function get(string $key): ?HttpRouteInterface; + + /** + * Get the current route. + */ + public function current(): HttpRouteInterface; +} diff --git a/src/Container/RouteContainerInterface.php b/src/Container/RouteContainerInterface.php new file mode 100644 index 00000000..ec146a7a --- /dev/null +++ b/src/Container/RouteContainerInterface.php @@ -0,0 +1,47 @@ + + */ +interface RouteContainerInterface extends Iterator, Countable +{ + /** + * Insert a route with a given key and priority. + * + * @param TKey $key + * @param TValue $value + */ + public function insert(string $key, RouteInterface $value, ?int $priority = null): void; + + /** + * Get a route by key. + * + * @param TKey $key + * @return TValue|null + */ + public function get(string $key): ?RouteInterface; + + /** + * Remove a route by key. + * + * @param TKey $key + */ + public function remove(string $key): void; + + /** + * Clear all routes. + */ + public function clear(): void; +} diff --git a/src/Container/SimpleRouteContainer.php b/src/Container/SimpleRouteContainer.php new file mode 100644 index 00000000..db628624 --- /dev/null +++ b/src/Container/SimpleRouteContainer.php @@ -0,0 +1,30 @@ + + */ +final class SimpleRouteContainer extends AbstractRouteContainer implements SimpleRouteContainerInterface +{ + public function insert(string $key, RouteInterface $value, ?int $priority = null): void + { + $this->setRoute($key, $value, $priority); + } + + public function get(string $key): ?RouteInterface + { + return $this->getRoute($key); + } + + public function current(): RouteInterface + { + return $this->getCurrentRoute(); + } +} diff --git a/src/Container/SimpleRouteContainerInterface.php b/src/Container/SimpleRouteContainerInterface.php new file mode 100644 index 00000000..5a7605fb --- /dev/null +++ b/src/Container/SimpleRouteContainerInterface.php @@ -0,0 +1,30 @@ + + */ +interface SimpleRouteContainerInterface extends RouteContainerInterface +{ + /** + * Insert a route with a given key and priority. + */ + public function insert(string $key, RouteInterface $value, ?int $priority = null): void; + + /** + * Get a route by key. + */ + public function get(string $key): ?RouteInterface; + + /** + * Get the current route. + */ + public function current(): RouteInterface; +} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 141bf75b..ed3a3c53 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -4,6 +4,6 @@ namespace Laminas\Router\Exception; -class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index 3cabbfe5..d63ab5ac 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -4,6 +4,6 @@ namespace Laminas\Router\Exception; -class RuntimeException extends \RuntimeException implements ExceptionInterface +final class RuntimeException extends \RuntimeException implements ExceptionInterface { } diff --git a/src/Http/Chain.php b/src/Http/Chain.php index d8ec9393..0a347c42 100644 --- a/src/Http/Chain.php +++ b/src/Http/Chain.php @@ -6,96 +6,67 @@ use ArrayObject; use Laminas\Router\Exception; -use Laminas\Router\PriorityList; -use Laminas\Router\RouteInterface; +use Laminas\Router\RouteConfigTrait; use Laminas\Router\RoutePluginManager; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; +use Psr\Container\ContainerExceptionInterface; +use Psr\Http\Message\ServerRequestInterface; use Traversable; use function array_diff_key; use function array_flip; -use function array_key_last; use function array_reverse; -use function assert; -use function is_array; -use function is_bool; -use function method_exists; -use function sprintf; +use function count; use function strlen; -/** - * @template TRoute of HttpRouteInterface - * @template-extends TreeRouteStack - */ -class Chain extends TreeRouteStack implements HttpRouteInterface +final class Chain extends TreeRouteStack implements HttpRouteInterface { + use RouteConfigTrait; + /** * Chain routes. * - * @var array + * @var iterable|null */ - protected $chainRoutes; + protected ?array $chainRoutes = null; /** * List of assembled parameters. - * - * @var array */ - protected $assembledParams = []; + protected array $assembledParams = []; /** * Create a new part route. * - * @param RoutePluginManager $routePlugins - * @param ArrayObject|null $prototypes + * @param ArrayObject|null $prototypes */ public function __construct(array $routes, RoutePluginManager $routePlugins, ?ArrayObject $prototypes = null) { - $this->chainRoutes = array_reverse($routes); - $this->routePluginManager = $routePlugins; - /** @var PriorityList $this->routes */ - $this->routes = new PriorityList(); - $this->prototypes = $prototypes; + parent::__construct($routePlugins); + + /** @psalm-var array $routes */ + $this->chainRoutes = array_reverse($routes); + if ($prototypes !== null) { + $this->prototypes = $prototypes; + } } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param mixed $options + * @inheritDoc * @throws Exception\InvalidArgumentException - * @return Part */ - public static function factory($options = []) + public static function factory(iterable $options = []): Chain { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['routes'])) { - throw new Exception\InvalidArgumentException('Missing "routes" in options array'); - } - - if (! isset($options['prototypes'])) { - $options['prototypes'] = null; - } + $options = self::processRouteOptions( + $options, + ['routes', 'route_plugins'], + ['prototypes' => null], + ); if ($options['routes'] instanceof Traversable) { - $options['routes'] = ArrayUtils::iteratorToArray($options['child_routes']); - } - - if (! isset($options['route_plugins'])) { - throw new Exception\InvalidArgumentException('Missing "route_plugins" in options array'); + $options['routes'] = self::iteratorToArray($options['routes']); } - return new static( + return new Chain( $options['routes'], $options['route_plugins'], $options['prototypes'] @@ -103,19 +74,14 @@ public static function factory($options = []) } /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @param int|null $pathOffset - * @return RouteMatch|null + * @inheritDoc + * @throws ContainerExceptionInterface */ - public function match(Request $request, $pathOffset = null, array $options = []) - { - if (! method_exists($request, 'getUri')) { - return; - } - + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { if ($pathOffset === null) { $mustTerminate = true; $pathOffset = 0; @@ -129,15 +95,12 @@ public function match(Request $request, $pathOffset = null, array $options = []) } $match = new RouteMatch([]); - $uri = $request->getUri(); - $pathLength = strlen($uri->getPath()); + $pathLength = strlen($request->getUri()->getPath()); foreach ($this->routes as $route) { - assert($route instanceof HttpRouteInterface); $subMatch = $route->match($request, $pathOffset, $options); - if ($subMatch === null) { - return; + return null; } $match->merge($subMatch); @@ -145,20 +108,17 @@ public function match(Request $request, $pathOffset = null, array $options = []) } if ($mustTerminate && $pathOffset !== $pathLength) { - return; + return null; } return $match; } /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed + * @inheritDoc + * @throws ContainerExceptionInterface */ - public function assemble(array $params = [], array $options = []) + public function assemble(array $params = [], array $options = []): string { if ($this->chainRoutes !== null) { $this->addRoutes($this->chainRoutes); @@ -167,15 +127,14 @@ public function assemble(array $params = [], array $options = []) $this->assembledParams = []; - $routes = ArrayUtils::iteratorToArray($this->routes); - $lastRouteKey = array_key_last($routes); - $path = ''; - - foreach ($routes as $key => $route) { - $chainOptions = $options; - $hasChild = isset($options['has_child']) && is_bool($options['has_child']) && $options['has_child']; + $count = count($this->routes); + $index = 0; + $path = ''; - $chainOptions['has_child'] = $hasChild || $key !== $lastRouteKey; + foreach ($this->routes as $route) { + $index++; + $chainOptions = $options; + $chainOptions['has_child'] = ($options['has_child'] ?? false) === true || $index !== $count; $path .= $route->assemble($params, $chainOptions); $params = array_diff_key($params, array_flip($route->getAssembledParams())); @@ -186,14 +145,8 @@ public function assemble(array $params = [], array $options = []) return $path; } - /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() + /** @inheritDoc */ + public function getAssembledParams(): array { return $this->assembledParams; } diff --git a/src/Http/Hostname.php b/src/Http/Hostname.php index 96e2bfbb..1d51a504 100644 --- a/src/Http/Hostname.php +++ b/src/Http/Hostname.php @@ -5,16 +5,12 @@ namespace Laminas\Router\Http; use Laminas\Router\Exception; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Laminas\Uri\UriInterface; -use Traversable; +use Laminas\Router\RouteConfigTrait; +use Laminas\Router\RoutePriorityTrait; +use Psr\Http\Message\ServerRequestInterface; use function array_merge; use function count; -use function is_array; -use function method_exists; use function preg_match; use function preg_quote; use function sprintf; @@ -22,7 +18,6 @@ /** * Hostname route. - * * Note: the following type is recursive, but Psalm doesn't understand array shape recursion (yet). For now, we only * represented recursion of the 'optional' part type to 1 level, to ease analysis. * @@ -40,57 +35,42 @@ * } * > */ -class Hostname implements HttpRouteInterface +final class Hostname implements HttpRouteInterface { + use RouteConfigTrait; + use RoutePriorityTrait; + /** * Parts of the route. - * - * @var Parts */ - protected $parts; + private readonly array $parts; /** * Regex used for matching the route. - * - * @var string */ - protected $regex; + private readonly string $regex; /** * Map from regex groups to parameter names. - * - * @var array */ - protected $paramMap = []; + private array $paramMap = []; /** * Default values. - * - * @var array */ - protected $defaults; + private readonly array $defaults; /** * List of assembled parameters. * * @var list */ - protected $assembledParams = []; - - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - * - * @var int|null - */ - public $priority; + private array $assembledParams = []; /** * Create a new hostname route. - * - * @param string $route */ - public function __construct($route, array $constraints = [], array $defaults = []) + public function __construct(string $route, array $constraints = [], array $defaults = []) { $this->defaults = $defaults; $this->parts = $this->parseRouteDefinition($route); @@ -98,48 +78,31 @@ public function __construct($route, array $constraints = [], array $defaults = [ } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return Hostname + * @inheritDoc * @throws Exception\InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []): Hostname { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['route'])) { - throw new Exception\InvalidArgumentException('Missing "route" in options array'); - } - - if (! isset($options['constraints'])) { - $options['constraints'] = []; - } - - if (! isset($options['defaults'])) { - $options['defaults'] = []; - } - - return new static($options['route'], $options['constraints'], $options['defaults']); + $options = self::processRouteOptions( + $options, + ['route'], + ['constraints' => [], 'defaults' => []], + ); + + return new static( + $options['route'], + $options['constraints'], + $options['defaults'] + ); } /** * Parse a route definition. * - * @param string $def - * @return Parts * @throws Exception\RuntimeException + * @return Parts */ - protected function parseRouteDefinition($def) + protected function parseRouteDefinition(string $def): array { $currentPos = 0; $length = strlen($def); @@ -204,13 +167,8 @@ protected function parseRouteDefinition($def) /** * Build the matching regex from parsed parts. - * - * @param Parts $parts - * @param int $groupIndex - * @return string - * @throws Exception\RuntimeException */ - protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1) + protected function buildRegex(array $parts, array $constraints, int &$groupIndex = 1): string { $regex = ''; @@ -235,7 +193,7 @@ protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1 break; case 'optional': - $regex .= '(?:' . $this->buildRegex($part[1], $constraints, $groupIndex) . ')?'; + $regex .= '(?:' . $this->buildRegex($part[1], $constraints, $groupIndex) . ')??'; break; } } @@ -246,14 +204,9 @@ protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1 /** * Build host. * - * @param Parts $parts * @param array $mergedParams - * @param bool $isOptional - * @return string - * @throws Exception\RuntimeException - * @throws Exception\InvalidArgumentException */ - protected function buildHost(array $parts, array $mergedParams, $isOptional) + protected function buildHost(array $parts, array $mergedParams, bool $isOptional): string { $host = ''; $skip = true; @@ -306,22 +259,13 @@ protected function buildHost(array $parts, array $mergedParams, $isOptional) return $host; } - /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @return RouteMatch|null - */ - public function match(Request $request) - { - if (! method_exists($request, 'getUri')) { - return null; - } - - /** @var UriInterface $uri */ - $uri = $request->getUri(); - $host = $uri->getHost() ?? ''; + /** @inheritDoc */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { + $host = $request->getUri()->getHost(); $result = preg_match('(^' . $this->regex . '$)', $host, $matches); @@ -340,14 +284,8 @@ public function match(Request $request) return new RouteMatch(array_merge($this->defaults, $params)); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + /** @inheritDoc */ + public function assemble(array $params = [], array $options = []): string { $this->assembledParams = []; @@ -366,13 +304,10 @@ public function assemble(array $params = [], array $options = []) } /** - * getAssembledParams(): defined by HttpRouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * + * @inheritDoc * @return list */ - public function getAssembledParams() + public function getAssembledParams(): array { return $this->assembledParams; } diff --git a/src/Http/HttpRouteInterface.php b/src/Http/HttpRouteInterface.php index d5d8a0bc..169352e9 100644 --- a/src/Http/HttpRouteInterface.php +++ b/src/Http/HttpRouteInterface.php @@ -4,23 +4,25 @@ namespace Laminas\Router\Http; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\RequestInterface as Request; +use Laminas\Router\RouteInterface as BaseRoute; +use Psr\Http\Message\ServerRequestInterface; /** - * Tree specific route interface. - * - * Note: the additional {@see self::match()} annotation is only here for documentation purposes, because we cannot - * change the signature of {@see self::match()} in the interface definition without breaking BC. - * - * @method RouteMatch|null match(Request $request, int|null $pathOffset = null, array $options = []) + * HTTP route interface. */ -interface HttpRouteInterface extends RouteInterface +interface HttpRouteInterface extends BaseRoute { + /** + * Match a given server request. + */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch; + /** * Get a list of parameters used while assembling. - * - * @return array */ - public function getAssembledParams(); + public function getAssembledParams(): array; } diff --git a/src/Http/HttpRouterFactory.php b/src/Http/HttpRouterFactory.php index b3099b9b..5bc83b86 100644 --- a/src/Http/HttpRouterFactory.php +++ b/src/Http/HttpRouterFactory.php @@ -4,47 +4,37 @@ namespace Laminas\Router\Http; +use Laminas\Router\RouteInterface; use Laminas\Router\RouterConfigTrait; -use Laminas\Router\RouteStackInterface; -use Laminas\ServiceManager\FactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; -class HttpRouterFactory implements FactoryInterface +final readonly class HttpRouterFactory implements FactoryInterface { use RouterConfigTrait; /** * Create and return the HTTP router - * * Retrieves the "router" key of the Config service, and uses it * to instantiate the router. Uses the TreeRouteStack implementation by * default. * - * @param string $name - * @param null|array $options - * @return RouteStackInterface + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function __invoke(ContainerInterface $container, $name, ?array $options = null) - { - $config = $container->has('config') ? $container->get('config') : []; + public function __invoke( + ContainerInterface $container, + string $requestedName, + ?array $options = null + ): RouteInterface { + $config = $container->has('config') ? (array) $container->get('config') : []; // Defaults - $class = TreeRouteStack::class; - $config = $config['router'] ?? []; - - return $this->createRouter($class, $config, $container); - } + $class = TreeRouteStack::class; + $routerConfig = (array) ($config['router'] ?? []); - /** - * Create and return RouteStackInterface instance - * - * For use with laminas-servicemanager v2; proxies to __invoke(). - * - * @return RouteStackInterface - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - return $this($serviceLocator, RouteStackInterface::class); + return $this->createRouter($class, $routerConfig, $container); } } diff --git a/src/Http/Literal.php b/src/Http/Literal.php index 6dea6621..fb383a18 100644 --- a/src/Http/Literal.php +++ b/src/Http/Literal.php @@ -5,102 +5,58 @@ namespace Laminas\Router\Http; use Laminas\Router\Exception; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Traversable; +use Laminas\Router\RouteConfigTrait; +use Laminas\Router\RoutePriorityTrait; +use Psr\Http\Message\ServerRequestInterface; -use function is_array; -use function method_exists; -use function sprintf; use function strlen; use function strpos; /** * Literal route. */ -class Literal implements HttpRouteInterface +final class Literal implements HttpRouteInterface { - /** - * Default values. - * - * @var array - */ - protected $defaults; - - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - * - * @var int|null - */ - public $priority; + use RouteConfigTrait; + use RoutePriorityTrait; /** * Create a new literal route. - * - * @param string $route */ public function __construct( - /** - * RouteInterface to match. - */ - protected $route, - array $defaults = [] + private readonly string $route, + private readonly array $defaults = [] ) { - $this->defaults = $defaults; } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return Literal + * @inheritDoc * @throws Exception\InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []): Literal { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['route'])) { - throw new Exception\InvalidArgumentException('Missing "route" in options array'); - } - - if (! isset($options['defaults'])) { - $options['defaults'] = []; - } - - return new static($options['route'], $options['defaults']); + $options = self::processRouteOptions( + $options, + ['route'], + ['defaults' => []], + ); + + return new Literal( + $options['route'], + $options['defaults'] + ); } - /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @param integer|null $pathOffset - * @return RouteMatch|null - */ - public function match(Request $request, $pathOffset = null) - { - if (! method_exists($request, 'getUri')) { - return null; - } - - $uri = $request->getUri(); - $path = $uri->getPath(); + /** @inheritDoc */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { + $path = $request->getUri()->getPath(); if ($pathOffset !== null) { - if ($pathOffset >= 0 && strlen((string) $path) >= $pathOffset && ! empty($this->route)) { + if ($pathOffset >= 0 && strlen($path) >= $pathOffset && ! empty($this->route)) { if (strpos($path, $this->route, $pathOffset) === $pathOffset) { return new RouteMatch($this->defaults, strlen($this->route)); } @@ -116,26 +72,14 @@ public function match(Request $request, $pathOffset = null) return null; } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + /** @inheritDoc */ + public function assemble(array $params = [], array $options = []): string { return $this->route; } - /** - * getAssembledParams(): defined by HttpRouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() + /** @inheritDoc */ + public function getAssembledParams(): array { return []; } diff --git a/src/Http/Method.php b/src/Http/Method.php index 9e4cb66c..70ccf58a 100644 --- a/src/Http/Method.php +++ b/src/Http/Method.php @@ -5,130 +5,75 @@ namespace Laminas\Router\Http; use Laminas\Router\Exception; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Traversable; +use Laminas\Router\RouteConfigTrait; +use Laminas\Router\RoutePriorityTrait; +use Psr\Http\Message\ServerRequestInterface; use function array_map; use function explode; use function in_array; -use function is_array; -use function method_exists; -use function sprintf; use function strtoupper; /** * Method route. */ -class Method implements HttpRouteInterface +final class Method implements HttpRouteInterface { - /** - * Default values. - * - * @var array - */ - protected $defaults; + use RouteConfigTrait; + use RoutePriorityTrait; - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - * - * @var int|null - */ - public $priority; + /** @var list */ + private readonly array $verbs; /** * Create a new method route. - * - * @param string $verb */ public function __construct( - /** - * Verb to match. - */ - protected $verb, - array $defaults = [] + string $verb, + private readonly array $defaults = [] ) { - $this->defaults = $defaults; + $this->verbs = array_map('trim', explode(',', strtoupper($verb))); } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return Method + * @inheritDoc * @throws Exception\InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []): Method { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['verb'])) { - throw new Exception\InvalidArgumentException('Missing "verb" in options array'); - } - - if (! isset($options['defaults'])) { - $options['defaults'] = []; - } - - return new static($options['verb'], $options['defaults']); + $options = self::processRouteOptions( + $options, + ['verb'], + ['defaults' => []], + ); + + return new Method( + $options['verb'], + $options['defaults'] + ); } - /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @return RouteMatch|null - */ - public function match(Request $request) - { - if (! method_exists($request, 'getMethod')) { - return null; - } - - $requestVerb = strtoupper($request->getMethod()); - $matchVerbs = explode(',', strtoupper($this->verb)); - $matchVerbs = array_map('trim', $matchVerbs); - - if (in_array($requestVerb, $matchVerbs)) { + /** @inheritDoc */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { + if (in_array(strtoupper($request->getMethod()), $this->verbs, true)) { return new RouteMatch($this->defaults); } return null; } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + /** @inheritDoc */ + public function assemble(array $params = [], array $options = []): string { - // The request method does not contribute to the path, thus nothing is returned. return ''; } - /** - * getAssembledParams(): defined by HttpRouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() + /** @inheritDoc */ + public function getAssembledParams(): array { return []; } diff --git a/src/Http/Part.php b/src/Http/Part.php index 1b89e370..1051d280 100644 --- a/src/Http/Part.php +++ b/src/Http/Part.php @@ -6,147 +6,100 @@ use ArrayObject; use Laminas\Router\Exception; -use Laminas\Router\PriorityList; -use Laminas\Router\RouteInterface; +use Laminas\Router\RouteConfigTrait; use Laminas\Router\RoutePluginManager; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Traversable; +use Psr\Container\ContainerExceptionInterface; +use Psr\Http\Message\ServerRequestInterface; use function array_diff_key; use function array_flip; use function is_array; -use function method_exists; -use function sprintf; +use function is_iterable; use function strlen; -/** - * @template TRoute of HttpRouteInterface - * @template-extends TreeRouteStack - */ -class Part extends TreeRouteStack implements HttpRouteInterface +final class Part extends TreeRouteStack implements HttpRouteInterface { - /** - * RouteInterface to match. - * - * @var TRoute - */ - protected $route; - - /** - * Child routes. - * - * @var mixed - */ - protected $childRoutes; + use RouteConfigTrait; /** * Create a new part route. * - * @param TRoute|iterable|string $route - * @param bool $mayTerminate - * @param array|null $childRoutes - * @param RoutePluginManager $routePlugins - * @param ArrayObject|null $prototypes - * @throws Exception\InvalidArgumentException + * @param ?iterable $childRoutes + * @param ?ArrayObject $prototypes + * @throws ContainerExceptionInterface */ public function __construct( - $route, - /** - * Whether the route may terminate. - */ - protected $mayTerminate, - RoutePluginManager $routePlugins, - ?array $childRoutes = null, + protected iterable|HttpRouteInterface $route, + private readonly bool $mayTerminate, + RoutePluginManager $routePluginManager, + protected ?array $childRoutes = null, ?ArrayObject $prototypes = null ) { - $this->routePluginManager = $routePlugins; + parent::__construct($routePluginManager); if (! $route instanceof HttpRouteInterface) { - $route = $this->routeFromArray($route); + $this->route = $this->routeFromSpec($route); } - if ($route instanceof self) { + if ($this->route instanceof self) { throw new Exception\InvalidArgumentException('Base route may not be a part route'); } - $this->route = $route; - $this->childRoutes = $childRoutes; - $this->prototypes = $prototypes; - /** @var PriorityList $this->routes */ - $this->routes = new PriorityList(); + if ($prototypes !== null) { + $this->prototypes = $prototypes; + } } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param mixed $options - * @return Part + * @inheritDoc * @throws Exception\InvalidArgumentException + * @throws ContainerExceptionInterface */ - public static function factory($options = []) + public static function factory(iterable $options = []): Part { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['route'])) { - throw new Exception\InvalidArgumentException('Missing "route" in options array'); - } - - if (! isset($options['route_plugins'])) { - throw new Exception\InvalidArgumentException('Missing "route_plugins" in options array'); - } - - if (! isset($options['prototypes'])) { - $options['prototypes'] = null; - } - - if (! isset($options['may_terminate'])) { - $options['may_terminate'] = false; - } + $options = self::processRouteOptions( + $options, + ['route', 'route_plugins'], + ['prototypes' => null, 'may_terminate' => false, 'child_routes' => null], + ); - if (! isset($options['child_routes']) || ! $options['child_routes']) { - $options['child_routes'] = null; + $childRoutes = null; + if (is_iterable($options['child_routes'] ?? null)) { + /** @var iterable $childRoutes */ + $childRoutes = self::iteratorToArray($options['child_routes']); } - if ($options['child_routes'] instanceof Traversable) { - $options['child_routes'] = ArrayUtils::iteratorToArray($options['child_routes']); + // Ensure prototypes is ArrayObject or null + $prototypes = $options['prototypes']; + if (is_array($prototypes)) { + $prototypes = new ArrayObject($prototypes); } - return new static( + return new Part( $options['route'], $options['may_terminate'], $options['route_plugins'], - $options['child_routes'], - $options['prototypes'] + $childRoutes, + $prototypes ); } /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @param integer|null $pathOffset - * @return RouteMatch|null + * @inheritDoc + * @throws ContainerExceptionInterface */ - public function match(Request $request, $pathOffset = null, array $options = []) - { + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { if ($pathOffset === null) { $pathOffset = 0; } $match = $this->route->match($request, $pathOffset, $options); - if ($match !== null && method_exists($request, 'getUri')) { + if ($match !== null) { if ($this->childRoutes !== null) { $this->addRoutes($this->childRoutes); $this->childRoutes = null; @@ -154,8 +107,7 @@ public function match(Request $request, $pathOffset = null, array $options = []) $nextOffset = $pathOffset + $match->getLength(); - $uri = $request->getUri(); - $pathLength = strlen($uri->getPath()); + $pathLength = strlen($request->getUri()->getPath()); if ($this->mayTerminate && $nextOffset === $pathLength) { return $match; @@ -164,7 +116,7 @@ public function match(Request $request, $pathOffset = null, array $options = []) if ( isset($options['translator']) && ! isset($options['locale']) - && null !== ($locale = $match->getParam('locale', null)) + && null !== ($locale = $match->getParam('locale')) ) { $options['locale'] = $locale; } @@ -182,14 +134,11 @@ public function match(Request $request, $pathOffset = null, array $options = []) } /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed + * @inheritDoc * @throws Exception\RuntimeException + * @throws ContainerExceptionInterface */ - public function assemble(array $params = [], array $options = []) + public function assemble(array $params = [], array $options = []): mixed { if ($this->childRoutes !== null) { $this->addRoutes($this->childRoutes); @@ -215,17 +164,12 @@ public function assemble(array $params = [], array $options = []) unset($options['has_child']); $options['only_return_path'] = true; + return $path . parent::assemble($params, $options); } - /** - * getAssembledParams(): defined by HttpRouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() + /** @inheritDoc */ + public function getAssembledParams(): array { // Part routes may not occur as base route of other part routes, so we // don't have to return anything here. diff --git a/src/Http/Placeholder.php b/src/Http/Placeholder.php index a42dbdca..79d601a9 100644 --- a/src/Http/Placeholder.php +++ b/src/Http/Placeholder.php @@ -5,97 +5,60 @@ namespace Laminas\Router\Http; use Laminas\Router\Exception; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Traversable; +use Laminas\Router\RouteConfigTrait; +use Laminas\Router\RoutePriorityTrait; +use Psr\Http\Message\ServerRequestInterface; use function is_array; -use function sprintf; /** * Placeholder route. */ -class Placeholder implements HttpRouteInterface +final class Placeholder implements HttpRouteInterface { - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - * - * @var int|null - */ - public $priority; + use RouteConfigTrait; + use RoutePriorityTrait; public function __construct(private readonly array $defaults) { } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return Placeholder + * @inheritDoc * @throws Exception\InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []): Placeholder { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } - - if (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['defaults'])) { - $options['defaults'] = []; - } + $options = self::processRouteOptions( + $options, + [], + ['defaults' => []], + ); if (! is_array($options['defaults'])) { throw new Exception\InvalidArgumentException('options[defaults] expected to be an array if set'); } - return new static($options['defaults']); + return new Placeholder($options['defaults']); } - /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @param integer|null $pathOffset - * @return RouteMatch|null - */ - public function match(Request $request, $pathOffset = null) - { + /** @inheritDoc */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { return new RouteMatch($this->defaults); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + /** @inheritDoc */ + public function assemble(array $params = [], array $options = []): string { return ''; } - /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() + /** @inheritDoc */ + public function getAssembledParams(): array { return []; } diff --git a/src/Http/Regex.php b/src/Http/Regex.php index 09f6383c..af786950 100644 --- a/src/Http/Regex.php +++ b/src/Http/Regex.php @@ -4,22 +4,17 @@ namespace Laminas\Router\Http; -use Laminas\Router\Exception; use Laminas\Router\Exception\InvalidArgumentException; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Traversable; +use Laminas\Router\RouteConfigTrait; +use Laminas\Router\RoutePriorityTrait; +use Psr\Http\Message\ServerRequestInterface; use function array_merge; -use function is_array; use function is_int; use function is_numeric; -use function method_exists; use function preg_match; use function rawurldecode; use function rawurlencode; -use function sprintf; use function str_contains; use function str_replace; use function strlen; @@ -27,101 +22,55 @@ /** * Regex route. */ -class Regex implements HttpRouteInterface +final class Regex implements HttpRouteInterface { - /** - * Default values. - * - * @var array - */ - protected $defaults; + use RouteConfigTrait; + use RoutePriorityTrait; /** * List of assembled parameters. - * - * @var array - */ - protected $assembledParams = []; - - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - * - * @var int|null */ - public $priority; + private array $assembledParams = []; /** * Create a new regex route. * - * @param string $regex - * @param string $spec + * @param string $regex Regex to match + * @param string $spec Specification for URL assembly. Parameters with substitutions should be denoted as "%key%" */ public function __construct( - /** - * Regex to match. - */ - protected $regex, - /** - * Specification for URL assembly. - * - * Parameters accepting substitutions should be denoted as "%key%" - */ - protected $spec, - array $defaults = [] + private readonly string $regex, + private readonly string $spec, + private readonly array $defaults = [] ) { - $this->defaults = $defaults; } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return Regex + * @inheritDoc * @throws InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []): Regex { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['regex'])) { - throw new Exception\InvalidArgumentException('Missing "regex" in options array'); - } - - if (! isset($options['spec'])) { - throw new Exception\InvalidArgumentException('Missing "spec" in options array'); - } - - if (! isset($options['defaults'])) { - $options['defaults'] = []; - } - - return new static($options['regex'], $options['spec'], $options['defaults']); + $options = self::processRouteOptions( + $options, + ['regex', 'spec'], + ['defaults' => []], + ); + + return new Regex( + $options['regex'], + $options['spec'], + $options['defaults'] + ); } - /** - * match(): defined by RouteInterface interface. - * - * @param int $pathOffset - * @return RouteMatch|null - */ - public function match(Request $request, $pathOffset = null) - { - if (! method_exists($request, 'getUri')) { - return; - } - - $uri = $request->getUri(); - $path = $uri->getPath(); + /** @inheritDoc */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { + $path = $request->getUri()->getPath(); if ($pathOffset !== null) { $result = preg_match('(\G' . $this->regex . ')', $path, $matches, 0, $pathOffset); @@ -130,7 +79,7 @@ public function match(Request $request, $pathOffset = null) } if (! $result) { - return; + return null; } $matchedLength = strlen($matches[0]); @@ -146,14 +95,8 @@ public function match(Request $request, $pathOffset = null) return new RouteMatch(array_merge($this->defaults, $matches), $matchedLength); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + /** @inheritDoc */ + public function assemble(array $params = [], array $options = []): string|array { $url = $this->spec; $mergedParams = array_merge($this->defaults, $params); @@ -172,14 +115,8 @@ public function assemble(array $params = [], array $options = []) return $url; } - /** - * getAssembledParams(): defined by HttpRouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() + /** @inheritDoc */ + public function getAssembledParams(): array { return $this->assembledParams; } diff --git a/src/Http/RouteInterface.php b/src/Http/RouteInterface.php deleted file mode 100644 index bb0977f8..00000000 --- a/src/Http/RouteInterface.php +++ /dev/null @@ -1,18 +0,0 @@ -matchedRouteName === null) { $this->matchedRouteName = $name; @@ -49,10 +41,8 @@ public function setMatchedRouteName($name) /** * Merge parameters from another match. - * - * @return RouteMatch */ - public function merge(RouteMatch $match) + public function merge(RouteMatch $match): RouteMatch { $this->params = array_merge($this->params, $match->getParams()); $this->length += $match->getLength(); @@ -64,10 +54,8 @@ public function merge(RouteMatch $match) /** * Get the matched path length. - * - * @return int */ - public function getLength() + public function getLength(): int { return $this->length; } diff --git a/src/Http/Scheme.php b/src/Http/Scheme.php index 92ec74b2..ca0734af 100644 --- a/src/Http/Scheme.php +++ b/src/Http/Scheme.php @@ -5,112 +5,57 @@ namespace Laminas\Router\Http; use Laminas\Router\Exception; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Traversable; - -use function is_array; -use function method_exists; -use function sprintf; +use Laminas\Router\RouteConfigTrait; +use Laminas\Router\RoutePriorityTrait; +use Psr\Http\Message\ServerRequestInterface; /** * Scheme route. */ -class Scheme implements HttpRouteInterface +final class Scheme implements HttpRouteInterface { - /** - * Default values. - * - * @var array - */ - protected $defaults; - - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - * - * @var int|null - */ - public $priority; + use RouteConfigTrait; + use RoutePriorityTrait; - /** - * Create a new scheme route. - * - * @param string $scheme - */ public function __construct( - /** - * Scheme to match. - */ - protected $scheme, - array $defaults = [] + private readonly string $scheme, + private readonly array $defaults = [] ) { - $this->defaults = $defaults; } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return Scheme + * @inheritDoc * @throws Exception\InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []): Scheme { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['scheme'])) { - throw new Exception\InvalidArgumentException('Missing "scheme" in options array'); - } - - if (! isset($options['defaults'])) { - $options['defaults'] = []; - } - - return new static($options['scheme'], $options['defaults']); + $options = self::processRouteOptions( + $options, + ['scheme'], + ['defaults' => []], + ); + + return new Scheme( + $options['scheme'], + $options['defaults'] + ); } - /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @return RouteMatch|null - */ - public function match(Request $request) - { - if (! method_exists($request, 'getUri')) { - return; - } - - $uri = $request->getUri(); - $scheme = $uri->getScheme(); - - if ($scheme !== $this->scheme) { - return; + /** @inheritDoc */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { + if ($request->getUri()->getScheme() !== $this->scheme) { + return null; } return new RouteMatch($this->defaults); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + /** @inheritDoc */ + public function assemble(array $params = [], array $options = []): string { if (isset($options['uri'])) { $options['uri']->setScheme($this->scheme); @@ -120,14 +65,8 @@ public function assemble(array $params = [], array $options = []) return ''; } - /** - * getAssembledParams(): defined by HttpRouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() + /** @inheritDoc */ + public function getAssembledParams(): array { return []; } diff --git a/src/Http/Segment.php b/src/Http/Segment.php index ebcb63fb..326e88c2 100644 --- a/src/Http/Segment.php +++ b/src/Http/Segment.php @@ -4,17 +4,15 @@ namespace Laminas\Router\Http; -use Laminas\I18n\Translator\TranslatorInterface as Translator; +use Laminas\I18n\Translator\Translator; use Laminas\Router\Exception; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Traversable; +use Laminas\Router\RouteConfigTrait; +use Laminas\Router\RoutePriorityTrait; +use Laminas\Translator\TranslatorInterface; +use Psr\Http\Message\ServerRequestInterface; use function array_merge; use function count; -use function is_array; -use function method_exists; use function preg_match; use function preg_quote; use function rawurldecode; @@ -27,14 +25,17 @@ /** * Segment route. */ -class Segment implements HttpRouteInterface +final class Segment implements HttpRouteInterface { + use RouteConfigTrait; + use RoutePriorityTrait; + /** * Cache for the encode output. * * @var array */ - protected static $cacheEncode = []; + protected static array $cacheEncode = []; /** * Map of allowed special chars in path segments. @@ -48,7 +49,7 @@ class Segment implements HttpRouteInterface * * @var array */ - protected static $urlencodeCorrectionMap = [ + protected static array $urlencodeCorrectionMap = [ '%21' => "!", // sub-delims '%24' => "$", // sub-delims '%26' => "&", // sub-delims @@ -70,60 +71,38 @@ class Segment implements HttpRouteInterface /** * Parts of the route. - * - * @var array */ - protected $parts; + private readonly array $parts; /** * Regex used for matching the route. - * - * @var string */ - protected $regex; + private readonly string $regex; /** * Map from regex groups to parameter names. - * - * @var array */ - protected $paramMap = []; + private array $paramMap = []; /** * Default values. - * - * @var array */ - protected $defaults; + private readonly array $defaults; /** * List of assembled parameters. - * - * @var array */ - protected $assembledParams = []; + private array $assembledParams = []; /** * Translation keys used in the regex. - * - * @var array - */ - protected $translationKeys = []; - - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - * - * @var int|null */ - public $priority; + private array $translationKeys = []; /** * Create a new regex route. - * - * @param string $route */ - public function __construct($route, array $constraints = [], array $defaults = []) + public function __construct(string $route, array $constraints = [], array $defaults = []) { $this->defaults = $defaults; $this->parts = $this->parseRouteDefinition($route); @@ -131,48 +110,30 @@ public function __construct($route, array $constraints = [], array $defaults = [ } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return Segment + * @inheritDoc * @throws Exception\InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []): Segment { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['route'])) { - throw new Exception\InvalidArgumentException('Missing "route" in options array'); - } - - if (! isset($options['constraints'])) { - $options['constraints'] = []; - } - - if (! isset($options['defaults'])) { - $options['defaults'] = []; - } + $options = self::processRouteOptions( + $options, + ['route'], + ['constraints' => [], 'defaults' => []], + ); - return new static($options['route'], $options['constraints'], $options['defaults']); + return new Segment( + $options['route'], + $options['constraints'], + $options['defaults'] + ); } /** * Parse a route definition. * - * @param string $def - * @return array * @throws Exception\RuntimeException */ - protected function parseRouteDefinition($def) + protected function parseRouteDefinition(string $def): array { $currentPos = 0; $length = strlen($def); @@ -243,11 +204,8 @@ protected function parseRouteDefinition($def) /** * Build the matching regex from parsed parts. - * - * @param int $groupIndex - * @return string */ - protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1) + protected function buildRegex(array $parts, array $constraints, int &$groupIndex = 1): string { $regex = ''; @@ -288,16 +246,18 @@ protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1 /** * Build a path. * - * @param bool $isOptional - * @param bool $hasChild - * @return string * @throws Exception\InvalidArgumentException * @throws Exception\RuntimeException */ - protected function buildPath(array $parts, array $mergedParams, $isOptional, $hasChild, array $options) - { + protected function buildPath( + array $parts, + array $mergedParams, + bool $isOptional, + bool $hasChild, + array $options + ): string { if ($this->translationKeys) { - if (! isset($options['translator']) || ! $options['translator'] instanceof Translator) { + if (! isset($options['translator']) || ! $options['translator'] instanceof TranslatorInterface) { throw new Exception\RuntimeException('No translator provided'); } @@ -363,22 +323,15 @@ protected function buildPath(array $parts, array $mergedParams, $isOptional, $ha } /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @param string|null $pathOffset - * @return RouteMatch|null + * @inheritDoc * @throws Exception\RuntimeException */ - public function match(Request $request, $pathOffset = null, array $options = []) - { - if (! method_exists($request, 'getUri')) { - return; - } - - $uri = $request->getUri(); - $path = $uri->getPath(); + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { + $path = $request->getUri()->getPath(); $regex = $this->regex; @@ -403,7 +356,7 @@ public function match(Request $request, $pathOffset = null, array $options = []) } if (! $result) { - return; + return null; } $matchedLength = strlen($matches[0]); @@ -418,14 +371,8 @@ public function match(Request $request, $pathOffset = null, array $options = []) return new RouteMatch(array_merge($this->defaults, $params), $matchedLength); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + /** @inheritDoc */ + public function assemble(array $params = [], array $options = []): string { $this->assembledParams = []; @@ -438,24 +385,16 @@ public function assemble(array $params = [], array $options = []) ); } - /** - * getAssembledParams(): defined by HttpRouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() + /** @inheritDoc */ + public function getAssembledParams(): array { return $this->assembledParams; } /** * Encode a path segment. - * - * @return string */ - protected function encode(string $value) + protected function encode(string $value): string { if (! isset(static::$cacheEncode[$value])) { static::$cacheEncode[$value] = rawurlencode($value); @@ -466,11 +405,8 @@ protected function encode(string $value) /** * Decode a path segment. - * - * @param string $value - * @return string */ - protected function decode($value) + protected function decode(string $value): string { return rawurldecode($value); } diff --git a/src/Http/TranslatorAwareTreeRouteStack.php b/src/Http/TranslatorAwareTreeRouteStack.php index a09e0e31..b85b7b2f 100644 --- a/src/Http/TranslatorAwareTreeRouteStack.php +++ b/src/Http/TranslatorAwareTreeRouteStack.php @@ -4,51 +4,36 @@ namespace Laminas\Router\Http; -use Laminas\I18n\Translator\TranslatorAwareInterface; -use Laminas\I18n\Translator\TranslatorInterface as Translator; use Laminas\Router\Exception; -use Laminas\Router\RouteInterface; -use Laminas\Stdlib\RequestInterface as Request; +use Laminas\Translator\TranslatorInterface as Translator; +use Psr\Http\Message\ServerRequestInterface; /** * Translator aware tree route stack. - * - * @template TRoute of HttpRouteInterface - * @template-extends TreeRouteStack */ -class TranslatorAwareTreeRouteStack extends TreeRouteStack implements TranslatorAwareInterface +final class TranslatorAwareTreeRouteStack extends TreeRouteStack { /** * Translator used for translatable segments. - * - * @var Translator */ - protected $translator; + protected ?Translator $translator = null; /** * Whether the translator is enabled. - * - * @var bool */ - protected $translatorEnabled = true; + protected bool $translatorEnabled = true; /** * Translator text domain to use. - * - * @var string */ - protected $translatorTextDomain = 'default'; - - /** - * match(): defined by RouteInterface - * - * @see RouteInterface::match() - * - * @param integer|null $pathOffset - * @return RouteMatch|null - */ - public function match(Request $request, $pathOffset = null, array $options = []) - { + protected string $translatorTextDomain = 'default'; + + /** @inheritDoc */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { if ($this->hasTranslator() && $this->isTranslatorEnabled() && ! isset($options['translator'])) { $options['translator'] = $this->getTranslator(); } @@ -61,15 +46,11 @@ public function match(Request $request, $pathOffset = null, array $options = []) } /** - * assemble(): defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed + * @inheritDoc * @throws Exception\InvalidArgumentException * @throws Exception\RuntimeException */ - public function assemble(array $params = [], array $options = []) + public function assemble(array $params = [], array $options = []): mixed { if ($this->hasTranslator() && $this->isTranslatorEnabled() && ! isset($options['translator'])) { $options['translator'] = $this->getTranslator(); @@ -86,12 +67,11 @@ public function assemble(array $params = [], array $options = []) * setTranslator(): defined by TranslatorAwareInterface. * * @see TranslatorAwareInterface::setTranslator() - * - * @param string $textDomain - * @return TreeRouteStack */ - public function setTranslator(?Translator $translator = null, $textDomain = null) - { + public function setTranslator( + ?Translator $translator = null, + ?string $textDomain = null + ): TranslatorAwareTreeRouteStack { $this->translator = $translator; if ($textDomain !== null) { @@ -103,63 +83,42 @@ public function setTranslator(?Translator $translator = null, $textDomain = null /** * getTranslator(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::getTranslator() - * - * @return Translator */ - public function getTranslator() + public function getTranslator(): ?Translator { return $this->translator; } /** * hasTranslator(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::hasTranslator() - * - * @return bool */ - public function hasTranslator() + public function hasTranslator(): bool { return $this->translator !== null; } /** * setTranslatorEnabled(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::setTranslatorEnabled() - * - * @param bool $enabled - * @return TreeRouteStack */ - public function setTranslatorEnabled($enabled = true) + public function setTranslatorEnabled(bool $enabled = true): TranslatorAwareTreeRouteStack { $this->translatorEnabled = $enabled; + return $this; } /** * isTranslatorEnabled(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::isTranslatorEnabled() - * - * @return bool */ - public function isTranslatorEnabled() + public function isTranslatorEnabled(): bool { return $this->translatorEnabled; } /** * setTranslatorTextDomain(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::setTranslatorTextDomain() - * - * @param string $textDomain - * @return self */ - public function setTranslatorTextDomain($textDomain = 'default') + public function setTranslatorTextDomain(string $textDomain = 'default'): TranslatorAwareTreeRouteStack { $this->translatorTextDomain = $textDomain; @@ -168,12 +127,8 @@ public function setTranslatorTextDomain($textDomain = 'default') /** * getTranslatorTextDomain(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::getTranslatorTextDomain() - * - * @return string */ - public function getTranslatorTextDomain() + public function getTranslatorTextDomain(): string { return $this->translatorTextDomain; } diff --git a/src/Http/TreeRouteStack.php b/src/Http/TreeRouteStack.php index 7f1ce8da..68134660 100644 --- a/src/Http/TreeRouteStack.php +++ b/src/Http/TreeRouteStack.php @@ -5,19 +5,25 @@ namespace Laminas\Router\Http; use ArrayObject; +use Laminas\Router\AbstractRouteStack; +use Laminas\Router\Container\HttpRouteContainer; +use Laminas\Router\Container\HttpRouteContainerInterface; +use Laminas\Router\Container\RouteContainerInterface; use Laminas\Router\Exception; use Laminas\Router\RouteInterface; -use Laminas\Router\RouteInvokableFactory; -use Laminas\Router\SimpleRouteStack; -use Laminas\ServiceManager\Config; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; +use Laminas\Router\RoutePluginManager; +use Laminas\Router\RouteStackInterface; +use Laminas\ServiceManager\ServiceManager; use Laminas\Uri\Http as HttpUri; +use Psr\Container\ContainerExceptionInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\UriInterface; use Traversable; use function array_merge; use function explode; use function is_array; +use function is_iterable; use function is_string; use function method_exists; use function rtrim; @@ -27,171 +33,109 @@ /** * Tree search implementation. * - * @template TRoute of HttpRouteInterface - * @template-extends SimpleRouteStack + * @extends AbstractRouteStack */ -class TreeRouteStack extends SimpleRouteStack +class TreeRouteStack extends AbstractRouteStack { /** * Base URL. - * - * @var string */ - protected $baseUrl; + protected ?string $baseUrl = null; /** * Request URI. - * - * @var HttpUri */ - protected $requestUri; + protected ?HttpUri $requestUri = null; /** * Prototype routes. - * * We use an ArrayObject in this case so we can easily pass it down the tree * by reference. * - * @var ArrayObject - */ - protected $prototypes; - - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - * - * @var int|null + * @var ArrayObject */ - public $priority; + protected ArrayObject $prototypes; + + /** @var HttpRouteContainerInterface */ + protected RouteContainerInterface $routes; + + public function __construct( + ?RoutePluginManager $routePluginManager = null, + ?HttpRouteContainerInterface $routes = null + ) { + $this->routePluginManager = $routePluginManager ?? new RoutePluginManager(new ServiceManager()); + $this->routes = $routes ?? new HttpRouteContainer(); + /** @var ArrayObject $prototypes */ + $prototypes = new ArrayObject(); + $this->prototypes = $prototypes; + $this->init(); + } /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return SimpleRouteStack + * @inheritDoc * @throws Exception\InvalidArgumentException + * @throws ContainerExceptionInterface */ - public static function factory($options = []) + public static function factory(iterable $options = []): RouteStackInterface { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); + if (! is_array($options)) { + $options = self::iteratorToArray($options); } - if (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); + $routePluginManager = $options['route_plugins'] ?? null; + if ($routePluginManager !== null && ! $routePluginManager instanceof RoutePluginManager) { + throw new Exception\InvalidArgumentException('route_plugins must be an instance of RoutePluginManager'); } - $instance = parent::factory($options); + $instance = new static($routePluginManager); - if (isset($options['prototypes'])) { + if (is_iterable($options['routes'] ?? null)) { + $instance->addRoutes($options['routes']); + } + + if (is_array($options['default_params'] ?? null)) { + $defaultParams = (array) $options['default_params']; + $instance->setDefaultParams($defaultParams); + } + + if (isset($options['prototypes']) && method_exists($instance, 'addPrototypes')) { $instance->addPrototypes($options['prototypes']); } return $instance; } - /** - * init(): defined by SimpleRouteStack. - * - * @see SimpleRouteStack::init() - */ - protected function init() - { - /** @var ArrayObject $this->prototypes */ - $this->prototypes = new ArrayObject(); - - (new Config([ - 'aliases' => [ - 'chain' => Chain::class, - 'Chain' => Chain::class, - 'hostname' => Hostname::class, - 'Hostname' => Hostname::class, - 'hostName' => Hostname::class, - 'HostName' => Hostname::class, - 'literal' => Literal::class, - 'Literal' => Literal::class, - 'method' => Method::class, - 'Method' => Method::class, - 'part' => Part::class, - 'Part' => Part::class, - 'regex' => Regex::class, - 'Regex' => Regex::class, - 'scheme' => Scheme::class, - 'Scheme' => Scheme::class, - 'segment' => Segment::class, - 'Segment' => Segment::class, - 'wildcard' => Wildcard::class, - 'Wildcard' => Wildcard::class, - 'wildCard' => Wildcard::class, - 'WildCard' => Wildcard::class, - ], - 'factories' => [ - Chain::class => RouteInvokableFactory::class, - Hostname::class => RouteInvokableFactory::class, - Literal::class => RouteInvokableFactory::class, - Method::class => RouteInvokableFactory::class, - Part::class => RouteInvokableFactory::class, - Regex::class => RouteInvokableFactory::class, - Scheme::class => RouteInvokableFactory::class, - Segment::class => RouteInvokableFactory::class, - Wildcard::class => RouteInvokableFactory::class, - - // v2 normalized names - 'laminasmvcrouterhttpchain' => RouteInvokableFactory::class, - 'laminasmvcrouterhttphostname' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpliteral' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpmethod' => RouteInvokableFactory::class, - 'laminasmvcrouterhttppart' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpregex' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpscheme' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpsegment' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpwildcard' => RouteInvokableFactory::class, - ], - ]))->configureServiceManager($this->routePluginManager); - } - /** * addRoute(): defined by RouteStackInterface interface. * - * @param string $name - * @param string|iterable|TRoute $route - * @param int $priority - * @return $this + * @throws ContainerExceptionInterface */ - public function addRoute($name, $route, $priority = null) - { + public function addRoute( + string $name, + string|iterable|RouteInterface $route, + ?int $priority = null + ): RouteStackInterface { if (! $route instanceof HttpRouteInterface) { - $route = $this->routeFromArray($route); + $route = $this->routeFromSpec($route); } - return parent::addRoute($name, $route, $priority); + $this->routes->insert($name, $route, $priority ?? $route->getPriority()); + + return $this; } /** - * @inheritDoc - * @param string|iterable $specs - * @return TRoute - * @throws Exception\InvalidArgumentException When route definition is not an array nor traversable. - * @throws Exception\InvalidArgumentException When chain routes are not an array nor traversable. - * @throws Exception\RuntimeException When a generated routes does not implement the HTTP route interface. + * @throws ContainerExceptionInterface */ - protected function routeFromArray($specs) + protected function routeFromSpec(string|iterable $specs): HttpRouteInterface { if (is_string($specs)) { - if (null === ($route = $this->getPrototype($specs))) { - throw new Exception\RuntimeException(sprintf('Could not find prototype with name %s', $specs)); - } + return $this->getPrototype($specs) + ?? throw new Exception\RuntimeException(sprintf('Could not find prototype with name %s', $specs)); + } - return $route; - } elseif ($specs instanceof Traversable) { - $specs = ArrayUtils::iteratorToArray($specs); - } elseif (! is_array($specs)) { - throw new Exception\InvalidArgumentException('Route definition must be an array or Traversable object'); + if ($specs instanceof Traversable) { + $specs = self::iteratorToArray($specs); } if (isset($specs['chain_routes'])) { @@ -212,13 +156,9 @@ protected function routeFromArray($specs) 'prototypes' => $this->prototypes, ]; - $route = $this->routePluginManager->get('chain', $options); + $route = $this->routePluginManager->build('chain', $options); } else { - $route = parent::routeFromArray($specs); - } - - if (! $route instanceof HttpRouteInterface) { - throw new Exception\RuntimeException('Given route does not implement HTTP route interface'); + $route = $this->routeFromIterable($specs); } if (isset($specs['child_routes'])) { @@ -230,10 +170,10 @@ protected function routeFromArray($specs) 'prototypes' => $this->prototypes, ]; - $priority = $route->priority ?? null; + $priority = $route->getPriority(); - $route = $this->routePluginManager->get('part', $options); - $route->priority = $priority; + $route = $this->routePluginManager->build('part', $options); + $route->setPriority($priority); } return $route; @@ -242,16 +182,12 @@ protected function routeFromArray($specs) /** * Add multiple prototypes at once. * - * @param iterable $routes - * @return $this + * @param iterable $routes * @throws Exception\InvalidArgumentException + * @throws ContainerExceptionInterface */ - public function addPrototypes($routes) + public function addPrototypes(iterable $routes): RouteStackInterface { - if (! is_array($routes) && ! $routes instanceof Traversable) { - throw new Exception\InvalidArgumentException('addPrototypes expects an array or Traversable set of routes'); - } - foreach ($routes as $name => $route) { $this->addPrototype($name, $route); } @@ -262,14 +198,12 @@ public function addPrototypes($routes) /** * Add a prototype. * - * @param string $name - * @param string|iterable|TRoute $route - * @return $this + * @throws ContainerExceptionInterface */ - public function addPrototype($name, $route) + public function addPrototype(string $name, iterable|HttpRouteInterface|string $route): static { if (! $route instanceof HttpRouteInterface) { - $route = $this->routeFromArray($route); + $route = $this->routeFromSpec($route); } $this->prototypes[$name] = $route; @@ -279,38 +213,27 @@ public function addPrototype($name, $route) /** * Get a prototype. - * - * @param string $name - * @return TRoute|null */ - public function getPrototype($name) + public function getPrototype(string $name): ?HttpRouteInterface { return $this->prototypes[$name] ?? null; } - /** - * match(): defined by RouteInterface - * - * @see RouteInterface::match() - * - * @param int|null $pathOffset - * @return RouteMatch|null - */ - public function match(Request $request, $pathOffset = null, array $options = []) - { - if (! method_exists($request, 'getUri')) { - return null; - } - + /** @inheritDoc */ + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): ?RouteMatch { if ($this->baseUrl === null && method_exists($request, 'getBaseUrl')) { $this->setBaseUrl($request->getBaseUrl()); } $uri = $request->getUri(); - $baseUrlLength = strlen((string) $this->baseUrl) ?: null; + $baseUrlLength = strlen($this->getBaseUrl()) ?: null; if ($pathOffset !== null) { - $baseUrlLength += $pathOffset; + $baseUrlLength = ($baseUrlLength ?? 0) + $pathOffset; } if ($this->requestUri === null) { @@ -318,7 +241,7 @@ public function match(Request $request, $pathOffset = null, array $options = []) } if ($baseUrlLength !== null) { - $pathLength = strlen((string) $uri->getPath()) - $baseUrlLength; + $pathLength = strlen($uri->getPath()) - $baseUrlLength; } else { $pathLength = null; } @@ -328,11 +251,11 @@ public function match(Request $request, $pathOffset = null, array $options = []) ($match = $route->match($request, $baseUrlLength, $options)) instanceof RouteMatch && ($pathLength === null || $match->getLength() === $pathLength) ) { - $match->setMatchedRouteName($name); + $match->setMatchedRouteName((string) $name); foreach ($this->defaultParams as $paramName => $value) { - if ($match->getParam($paramName) === null) { - $match->setParam($paramName, $value); + if ($match->getParam((string) $paramName) === null) { + $match->setParam((string) $paramName, $value); } } @@ -344,15 +267,11 @@ public function match(Request $request, $pathOffset = null, array $options = []) } /** - * assemble(): defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed + * @inheritDoc * @throws Exception\InvalidArgumentException * @throws Exception\RuntimeException */ - public function assemble(array $params = [], array $options = []) + public function assemble(array $params = [], array $options = []): mixed { if (! isset($options['name'])) { throw new Exception\InvalidArgumentException('Missing "name" option'); @@ -361,7 +280,7 @@ public function assemble(array $params = [], array $options = []) $names = explode('/', $options['name'], 2); $route = $this->routes->get($names[0]); - if (! $route) { + if ($route === null) { throw new Exception\RuntimeException(sprintf('Route with name "%s" not found', $names[0])); } @@ -378,7 +297,7 @@ public function assemble(array $params = [], array $options = []) } if (isset($options['only_return_path']) && $options['only_return_path']) { - return $this->baseUrl . $route->assemble(array_merge($this->defaultParams, $params), $options); + return $this->getBaseUrl() . $route->assemble(array_merge($this->defaultParams, $params), $options); } if (! isset($options['uri']) || ! $options['uri'] instanceof HttpUri) { @@ -399,7 +318,7 @@ public function assemble(array $params = [], array $options = []) $uri = $options['uri']; } - $path = $this->baseUrl . $route->assemble(array_merge($this->defaultParams, $params), $options); + $path = $this->getBaseUrl() . $route->assemble(array_merge($this->defaultParams, $params), $options); if (isset($options['query'])) { $uri->setQuery($options['query']); @@ -411,7 +330,7 @@ public function assemble(array $params = [], array $options = []) if ( (isset($options['force_canonical']) - && $options['force_canonical']) + && $options['force_canonical']) || $uri->getHost() !== null || $uri->getScheme() !== null ) { @@ -449,43 +368,43 @@ public function assemble(array $params = [], array $options = []) /** * Set the base URL. - * - * @param string $baseUrl - * @return self */ - public function setBaseUrl($baseUrl) + public function setBaseUrl(string $baseUrl): static { $this->baseUrl = rtrim($baseUrl, '/'); + return $this; } /** * Get the base URL. - * - * @return string */ - public function getBaseUrl() + public function getBaseUrl(): string { - return $this->baseUrl; + return $this->baseUrl ?? ''; } /** * Set the request URI. * - * @return TreeRouteStack + * Accepts both Laminas\Uri\Http and PSR-7 UriInterface. + * PSR-7 URIs are converted to Laminas\Uri\Http internally. */ - public function setRequestUri(HttpUri $uri) + public function setRequestUri(HttpUri|UriInterface $uri): static { + if (! $uri instanceof HttpUri) { + $uri = new HttpUri((string) $uri); + } + $this->requestUri = $uri; + return $this; } /** * Get the request URI. - * - * @return HttpUri */ - public function getRequestUri() + public function getRequestUri(): ?HttpUri { return $this->requestUri; } diff --git a/src/Http/Wildcard.php b/src/Http/Wildcard.php deleted file mode 100644 index 297d4a26..00000000 --- a/src/Http/Wildcard.php +++ /dev/null @@ -1,203 +0,0 @@ -defaults = $defaults; - } - - /** - * factory(): defined by RouteInterface interface. - * - * @see RouteInterface::factory() - * - * @param iterable $options - * @return Wildcard - * @throws Exception\InvalidArgumentException - */ - public static function factory($options = []) - { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); - } - - if (! isset($options['key_value_delimiter'])) { - $options['key_value_delimiter'] = '/'; - } - - if (! isset($options['param_delimiter'])) { - $options['param_delimiter'] = '/'; - } - - if (! isset($options['defaults'])) { - $options['defaults'] = []; - } - - return new static($options['key_value_delimiter'], $options['param_delimiter'], $options['defaults']); - } - - /** - * match(): defined by RouteInterface interface. - * - * @see RouteInterface::match() - * - * @param integer|null $pathOffset - * @return RouteMatch|null - */ - public function match(Request $request, $pathOffset = null) - { - if (! method_exists($request, 'getUri')) { - return null; - } - - $uri = $request->getUri(); - $path = $uri->getPath() ?: ''; - - if ($path === '/') { - $path = ''; - } - - if ($pathOffset !== null) { - $path = substr($path, $pathOffset) ?: ''; - } - - $matches = []; - $params = explode($this->paramDelimiter, $path); - - if (count($params) > 1 && ($params[0] !== '' || end($params) === '')) { - return null; - } - - if ($this->keyValueDelimiter === $this->paramDelimiter) { - $count = count($params); - - for ($i = 1; $i < $count; $i += 2) { - if (isset($params[$i + 1])) { - $matches[rawurldecode($params[$i])] = rawurldecode($params[$i + 1]); - } - } - } else { - array_shift($params); - - foreach ($params as $param) { - $param = explode($this->keyValueDelimiter, $param, 2); - - if (isset($param[1])) { - $matches[rawurldecode($param[0])] = rawurldecode($param[1]); - } - } - } - - return new RouteMatch(array_merge($this->defaults, $matches), strlen($path)); - } - - /** - * assemble(): Defined by RouteInterface interface. - * - * @see RouteInterface::assemble() - * - * @return mixed - */ - public function assemble(array $params = [], array $options = []) - { - $elements = []; - $mergedParams = array_merge($this->defaults, $params); - $this->assembledParams = []; - - if ($mergedParams) { - foreach ($mergedParams as $key => $value) { - if (! is_scalar($value)) { - continue; - } - - $elements[] = rawurlencode($key) - . $this->keyValueDelimiter - . rawurlencode((string) $value); - $this->assembledParams[] = $key; - } - - return $this->paramDelimiter . implode($this->paramDelimiter, $elements); - } - - return ''; - } - - /** - * getAssembledParams(): defined by HttpRouteInterface interface. - * - * @see HttpRouteInterface::getAssembledParams - * - * @return array - */ - public function getAssembledParams() - { - return $this->assembledParams; - } -} diff --git a/src/Module.php b/src/Module.php deleted file mode 100644 index df229843..00000000 --- a/src/Module.php +++ /dev/null @@ -1,26 +0,0 @@ - $provider->getDependencyConfig(), - 'route_manager' => $provider->getRouteManagerConfig(), - 'router' => ['routes' => []], - ]; - } -} diff --git a/src/PriorityList.php b/src/PriorityList.php deleted file mode 100644 index 086e3727..00000000 --- a/src/PriorityList.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ -class PriorityList extends StdlibPriorityList -{ -} diff --git a/src/RouteConfigTrait.php b/src/RouteConfigTrait.php new file mode 100644 index 00000000..11ff8e91 --- /dev/null +++ b/src/RouteConfigTrait.php @@ -0,0 +1,83 @@ +|Traversable $iterator + * @return array + */ + public static function iteratorToArray(iterable $iterator, bool $recursive = true): array + { + if (! $recursive) { + return is_array($iterator) ? $iterator : iterator_to_array($iterator); + } + + if (! is_array($iterator)) { + if (method_exists($iterator, 'toArray')) { + return $iterator->toArray(); + } else { + $array = []; + /** @psalm-suppress MixedAssignment, MixedArrayOffset */ + foreach ($iterator as $key => $value) { + $array[$key] = is_iterable($value) + ? static::iteratorToArray($value) + : $value; + } + + return $array; + } + } + + return array_map(function ($value) { + return is_iterable($value) + ? static::iteratorToArray($value) + : $value; + }, $iterator); + } + + /** + * Provide completed array options for specific routes. + * + * @param array $requiredOptions + * @param array $defaultOptions + * @return array + */ + private static function processRouteOptions( + iterable $options, + array $requiredOptions = [], + array $defaultOptions = [] + ): array { + if (! is_array($options)) { + $options = self::iteratorToArray($options); + } + + foreach ($requiredOptions as $requiredOption) { + if (! isset($options[$requiredOption])) { + throw new Exception\InvalidArgumentException(sprintf('Missing "%s" option', $requiredOption)); + } + } + + foreach ($defaultOptions as $defaultOption => $defaultValue) { + if (! isset($options[$defaultOption])) { + $options[$defaultOption] = $defaultValue; + } + } + + return $options; + } +} diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 7e921617..63546e38 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -4,39 +4,35 @@ namespace Laminas\Router; -use Laminas\Stdlib\RequestInterface as Request; +use Psr\Http\Message\ServerRequestInterface; /** * RouteInterface interface. */ interface RouteInterface { - /** - * Priority used for route stacks. - * - * @var int - * public $priority; - */ - /** * Create a new route with given options. - * - * @param iterable $options - * @return RouteInterface */ - public static function factory($options = []); + public static function factory(iterable $options = []): RouteInterface; /** * Match a given request. - * - * @return RouteMatch|null */ - public function match(Request $request); + public function match(ServerRequestInterface $request): ?RouteMatch; /** * Assemble the route. - * - * @return mixed */ - public function assemble(array $params = [], array $options = []); + public function assemble(array $params = [], array $options = []): mixed; + + /** + * Set the route priority. + */ + public function setPriority(?int $priority): void; + + /** + * Get the route priority. + */ + public function getPriority(): int; } diff --git a/src/RouteInvokableFactory.php b/src/RouteInvokableFactory.php index 670691fb..1f899d78 100644 --- a/src/RouteInvokableFactory.php +++ b/src/RouteInvokableFactory.php @@ -4,13 +4,11 @@ namespace Laminas\Router; -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; use Psr\Container\ContainerInterface; use function class_exists; use function is_subclass_of; -use function sprintf; /** * Specialized invokable/abstract factory for use with RoutePluginManager. @@ -18,64 +16,33 @@ * Can be mapped directly to specific route plugin names, or used as an * abstract factory to map FQCN services to invokables. */ -class RouteInvokableFactory implements - AbstractFactoryInterface +final readonly class RouteInvokableFactory implements AbstractFactoryInterface { /** - * Can we create a route instance with the given name? (v3) + * Can we create a route instance with the given name? * - * Only works for FQCN $routeName values, for classes that implement RouteInterface. - * - * @param string $requestedName - * @return bool + * Only works for FQCN $requestedName values, for classes that implement RouteInterface. */ - public function canCreate(ContainerInterface $container, $requestedName) + public function canCreate(ContainerInterface $container, string $requestedName): bool { - if (! class_exists($requestedName)) { - return false; - } - - if (! is_subclass_of($requestedName, RouteInterface::class)) { - return false; - } - - return true; + return class_exists($requestedName) && is_subclass_of($requestedName, RouteInterface::class); } /** * Create and return a RouteInterface instance. - * - * If the specified $routeName class does not exist or does not implement + * If the specified $requestedName class does not exist or does not implement * RouteInterface, this method will raise an exception. - * * Otherwise, it uses the class' `factory()` method with the provided * $options to produce an instance. - * - * @param string $requestedName - * @param null|array $options - * @return RouteInterface */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) - { + public function __invoke( + ContainerInterface $container, + string $requestedName, + ?array $options = null + ): RouteInterface { $options ??= []; - if (! class_exists($requestedName)) { - throw new ServiceNotCreatedException(sprintf( - '%s: failed retrieving invokable class "%s"; class does not exist', - self::class, - $requestedName - )); - } - - if (! is_subclass_of($requestedName, RouteInterface::class)) { - throw new ServiceNotCreatedException(sprintf( - '%s: failed retrieving invokable class "%s"; class does not implement %s', - self::class, - $requestedName, - RouteInterface::class - )); - } - + /** @psalm-var class-string $requestedName */ return $requestedName::factory($options); } } diff --git a/src/RouteMatch.php b/src/RouteMatch.php index 75db5ac7..1f86191c 100644 --- a/src/RouteMatch.php +++ b/src/RouteMatch.php @@ -9,21 +9,17 @@ /** * RouteInterface match. */ -class RouteMatch +class RouteMatch implements RouteMatchInterface { /** * Match parameters. - * - * @var array */ - protected $params = []; + protected array $params = []; /** * Matched route name. - * - * @var string */ - protected $matchedRouteName; + protected ?string $matchedRouteName = null; /** * Create a RouteMatch with given parameters. @@ -35,11 +31,8 @@ public function __construct(array $params) /** * Set name of matched route. - * - * @param string $name - * @return RouteMatch */ - public function setMatchedRouteName($name) + public function setMatchedRouteName(string $name): RouteMatchInterface { $this->matchedRouteName = $name; return $this; @@ -47,22 +40,16 @@ public function setMatchedRouteName($name) /** * Get name of matched route. - * - * @return string */ - public function getMatchedRouteName() + public function getMatchedRouteName(): ?string { return $this->matchedRouteName; } /** * Set a parameter. - * - * @param string $name - * @param mixed $value - * @return RouteMatch */ - public function setParam($name, $value) + public function setParam(string $name, mixed $value): RouteMatchInterface { $this->params[$name] = $value; return $this; @@ -71,21 +58,17 @@ public function setParam($name, $value) /** * Get all parameters. * - * @return array + * @return array */ - public function getParams() + public function getParams(): array { return $this->params; } /** * Get a specific parameter. - * - * @param string $name - * @param mixed $default - * @return mixed */ - public function getParam($name, $default = null) + public function getParam(string $name, mixed $default = null): mixed { if (array_key_exists($name, $this->params)) { return $this->params[$name]; diff --git a/src/RouteMatchInterface.php b/src/RouteMatchInterface.php new file mode 100644 index 00000000..8f5c8760 --- /dev/null +++ b/src/RouteMatchInterface.php @@ -0,0 +1,38 @@ + + */ + public function getParams(): array; + + /** + * Get a specific parameter. + */ + public function getParam(string $name, mixed $default = null): mixed; +} diff --git a/src/RoutePluginManager.php b/src/RoutePluginManager.php index 7b1c64ec..a680221f 100644 --- a/src/RoutePluginManager.php +++ b/src/RoutePluginManager.php @@ -4,23 +4,30 @@ namespace Laminas\Router; +use Laminas\Router\Http\Chain; +use Laminas\Router\Http\Hostname; +use Laminas\Router\Http\Literal; +use Laminas\Router\Http\Method; +use Laminas\Router\Http\Part; +use Laminas\Router\Http\Placeholder; +use Laminas\Router\Http\Regex; +use Laminas\Router\Http\Scheme; +use Laminas\Router\Http\Segment; use Laminas\ServiceManager\AbstractPluginManager; -use Laminas\ServiceManager\ConfigInterface; use Laminas\ServiceManager\Exception\InvalidServiceException; use Laminas\ServiceManager\ServiceManager; use Psr\Container\ContainerInterface; use function array_merge; +use function array_replace_recursive; use function get_debug_type; use function sprintf; /** * Plugin manager implementation for routes - * * Enforces that routes retrieved are instances of RouteInterface. It overrides * configure() to map invokables to the component-specific * RouteInvokableFactory. - * * The manager is marked to not share by default, in order to allow multiple * route instances of the same type. * @@ -30,51 +37,72 @@ * @extends AbstractPluginManager * @psalm-import-type ServiceManagerConfiguration from ServiceManager */ -class RoutePluginManager extends AbstractPluginManager +final class RoutePluginManager extends AbstractPluginManager { + private const CONFIGURATION = [ + 'aliases' => [ + 'chain' => Chain::class, + 'Chain' => Chain::class, + 'hostname' => Hostname::class, + 'Hostname' => Hostname::class, + 'hostName' => Hostname::class, + 'HostName' => Hostname::class, + 'literal' => Literal::class, + 'Literal' => Literal::class, + 'method' => Method::class, + 'Method' => Method::class, + 'part' => Part::class, + 'Part' => Part::class, + 'placeholder' => Placeholder::class, + 'Placeholder' => Placeholder::class, + 'regex' => Regex::class, + 'Regex' => Regex::class, + 'scheme' => Scheme::class, + 'Scheme' => Scheme::class, + 'segment' => Segment::class, + 'Segment' => Segment::class, + ], + 'factories' => [ + Chain::class => RouteInvokableFactory::class, + Hostname::class => RouteInvokableFactory::class, + Literal::class => RouteInvokableFactory::class, + Method::class => RouteInvokableFactory::class, + Part::class => RouteInvokableFactory::class, + Placeholder::class => RouteInvokableFactory::class, + Regex::class => RouteInvokableFactory::class, + Scheme::class => RouteInvokableFactory::class, + Segment::class => RouteInvokableFactory::class, + ], + ]; + /** * Only RouteInterface instances are valid * * @var class-string */ - protected $instanceOf = RouteInterface::class; - - /** - * Do not share instances. (v3) - * - * @var bool - */ - protected $shareByDefault = false; + protected string $instanceOf = RouteInterface::class; /** - * Do not share instances. (v2) - * - * @var bool + * Do not share instances. */ - protected $sharedByDefault = false; + protected bool $shareByDefault = false; /** - * Constructor - * - * Ensure that the instance is seeded with the RouteInvokableFactory as an - * abstract factory. - * - * @param ContainerInterface|ConfigInterface $configOrContainerInstance - * @psalm-param ServiceManagerConfiguration $v3config + * @param ServiceManagerConfiguration $config */ - public function __construct($configOrContainerInstance, array $v3config = []) + public function __construct(ContainerInterface $creationContext, array $config = []) { - $this->addAbstractFactory(RouteInvokableFactory::class); - parent::__construct($configOrContainerInstance, $v3config); + $config = array_replace_recursive(self::CONFIGURATION, $config); + parent::__construct($creationContext, $config); } /** - * Validate a route plugin. (v2) + * Validate a route plugin. * * @throws InvalidServiceException * @psalm-assert InstanceType $instance */ - public function validate(mixed $instance) + public function validate(mixed $instance): void { if (! $instance instanceof $this->instanceOf) { throw new InvalidServiceException(sprintf( @@ -86,28 +114,7 @@ public function validate(mixed $instance) } /** - * Validate a route plugin. (v2) - * - * @param InstanceType $plugin - * @throws Exception\RuntimeException - * @psalm-assert InstanceType $instance - */ - public function validatePlugin($plugin) - { - try { - $this->validate($plugin); - } catch (InvalidServiceException $e) { - throw new Exception\RuntimeException( - $e->getMessage(), - $e->getCode(), - $e - ); - } - } - - /** - * Pre-process configuration. (v3) - * + * Pre-process configuration. * Checks for invokables, and, if found, maps them to the * component-specific RouteInvokableFactory; removes the invokables entry * before passing to the parent. @@ -115,9 +122,9 @@ public function validatePlugin($plugin) * @psalm-param ServiceManagerConfiguration $config * @return $this */ - public function configure(array $config) + public function configure(array $config): static { - if (isset($config['invokables']) && ! empty($config['invokables'])) { + if (! empty($config['invokables'])) { $aliases = $this->createAliasesForInvokables($config['invokables']); $factories = $this->createFactoriesForInvokables($config['invokables']); @@ -139,17 +146,16 @@ public function configure(array $config) return $this; } - /** - * Create aliases for invokable classes. - * - * If an invokable service name does not match the class it maps to, this - * creates an alias to the class (which will later be mapped as an - * invokable factory). - * - * @param array $invokables - * @return array - */ - protected function createAliasesForInvokables(array $invokables) + /** + * Create aliases for invokable classes. + * If an invokable service name does not match the class it maps to, this + * creates an alias to the class (which will later be mapped as an + * invokable factory). + * + * @param array $invokables + * @return array + */ + protected function createAliasesForInvokables(array $invokables): array { $aliases = []; foreach ($invokables as $name => $class) { @@ -158,12 +164,12 @@ protected function createAliasesForInvokables(array $invokables) } $aliases[$name] = $class; } + return $aliases; } /** * Create invokable factories for invokable classes. - * * If an invokable service name does not match the class it maps to, this * creates an invokable factory entry for the class name; otherwise, it * creates an invokable factory for the entry name. @@ -171,7 +177,7 @@ protected function createAliasesForInvokables(array $invokables) * @param array $invokables * @return array */ - protected function createFactoriesForInvokables(array $invokables) + protected function createFactoriesForInvokables(array $invokables): array { $factories = []; foreach ($invokables as $name => $class) { @@ -182,6 +188,7 @@ protected function createFactoriesForInvokables(array $invokables) $factories[$class] = RouteInvokableFactory::class; } + return $factories; } } diff --git a/src/RoutePluginManagerFactory.php b/src/RoutePluginManagerFactory.php index 4def78a0..95647222 100644 --- a/src/RoutePluginManagerFactory.php +++ b/src/RoutePluginManagerFactory.php @@ -5,20 +5,42 @@ namespace Laminas\Router; use Laminas\ServiceManager\Factory\FactoryInterface; +use Laminas\ServiceManager\ServiceManager; use Psr\Container\ContainerInterface; -class RoutePluginManagerFactory implements FactoryInterface +use function is_array; + +/** + * @psalm-import-type ServiceManagerConfiguration from ServiceManager + */final class RoutePluginManagerFactory implements FactoryInterface { /** * Create and return a route plugin manager. - * - * @param string $requestedName - * @param null|array $options - * @return RoutePluginManager */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) - { - $options ??= []; - return new RoutePluginManager($container, $options); + public function __invoke( + ContainerInterface $container, + string $requestedName, + ?array $options = null + ): RoutePluginManager { + // If this is in a laminas-mvc application, the ServiceListener will inject + // merged configuration during bootstrap. + if ($container->has('ServiceListener')) { + return new RoutePluginManager($container); + } + + // If we do not have a config service, nothing more to do + if (! $container->has('config')) { + return new RoutePluginManager($container, $options ?? []); + } + + $config = $container->get('config'); + + // If we do not have router configuration, nothing more to do + if (! isset($config['route_types']) || ! is_array($config['route_types'])) { + return new RoutePluginManager($container, $options ?? []); + } + + /** @psalm-var ServiceManagerConfiguration $config['route_types'] */ + return new RoutePluginManager($container, $config['route_types']); } } diff --git a/src/RoutePriorityTrait.php b/src/RoutePriorityTrait.php new file mode 100644 index 00000000..375ec886 --- /dev/null +++ b/src/RoutePriorityTrait.php @@ -0,0 +1,20 @@ +priority = $priority; + } + + public function getPriority(): int + { + return $this->priority ?? 0; + } +} diff --git a/src/RouteStackInterface.php b/src/RouteStackInterface.php index f7c86cef..341c016f 100644 --- a/src/RouteStackInterface.php +++ b/src/RouteStackInterface.php @@ -4,42 +4,29 @@ namespace Laminas\Router; -/** - * @template TRoute of RouteInterface - */ interface RouteStackInterface extends RouteInterface { /** * Add a route to the stack. - * - * @param string $name - * @param iterable|TRoute $route - * @param int $priority - * @return static */ - public function addRoute($name, $route, $priority = null); + public function addRoute(string $name, iterable|RouteInterface $route, ?int $priority = null): RouteInterface; /** * Add multiple routes to the stack. * - * @param iterable $routes - * @return static + * @param iterable $routes */ - public function addRoutes($routes); + public function addRoutes(iterable $routes): RouteStackInterface; /** * Remove a route from the stack. - * - * @param string $name - * @return static */ - public function removeRoute($name); + public function removeRoute(string $name): RouteStackInterface; /** * Remove all routes from the stack and set new ones. * - * @param iterable $routes - * @return static + * @param iterable $routes */ - public function setRoutes($routes); + public function setRoutes(iterable $routes): RouteStackInterface; } diff --git a/src/RouterConfigTrait.php b/src/RouterConfigTrait.php index a834ff1e..cdad443e 100644 --- a/src/RouterConfigTrait.php +++ b/src/RouterConfigTrait.php @@ -4,7 +4,9 @@ namespace Laminas\Router; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use function class_exists; use function sprintf; @@ -14,10 +16,9 @@ trait RouterConfigTrait /** * Create and return a router instance, by calling the appropriate factory. * - * @param string $class - * @return RouteInterface + * @throws ContainerExceptionInterface|NotFoundExceptionInterface */ - private function createRouter($class, array $config, ContainerInterface $container) + private function createRouter(string $class, array $config, ContainerInterface $container): RouteInterface { // Obtain the configured router class, if any if (isset($config['router_class']) && class_exists($config['router_class'])) { @@ -25,9 +26,11 @@ private function createRouter($class, array $config, ContainerInterface $contain } // Inject the route plugins - if (! isset($config['route_plugins'])) { - $routePluginManager = $container->get('RoutePluginManager'); - $config['route_plugins'] = $routePluginManager; + if (! isset($config['route_plugins']) && $container->has('RoutePluginManager')) { + $routePluginManager = $container->get('RoutePluginManager'); + if ($routePluginManager instanceof RoutePluginManager) { + $config['route_plugins'] = $routePluginManager; + } } // Obtain an instance diff --git a/src/RouterFactory.php b/src/RouterFactory.php index 8314bb7e..0cca8e42 100644 --- a/src/RouterFactory.php +++ b/src/RouterFactory.php @@ -5,20 +5,21 @@ namespace Laminas\Router; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; -class RouterFactory implements FactoryInterface +final readonly class RouterFactory implements FactoryInterface { /** * Create and return the router - * * Delegates to the HttpRouter service. * - * @param string $name - * @param null|array $options + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @return RouteStackInterface */ - public function __invoke(ContainerInterface $container, $name, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed { return $container->get('HttpRouter'); } diff --git a/src/SimpleRouteStack.php b/src/SimpleRouteStack.php index ea9a8154..0d767a09 100644 --- a/src/SimpleRouteStack.php +++ b/src/SimpleRouteStack.php @@ -4,267 +4,70 @@ namespace Laminas\Router; +use Laminas\Router\Container\RouteContainerInterface; +use Laminas\Router\Container\SimpleRouteContainer; +use Laminas\Router\Container\SimpleRouteContainerInterface; use Laminas\ServiceManager\ServiceManager; -use Laminas\Stdlib\ArrayUtils; -use Laminas\Stdlib\RequestInterface as Request; -use Traversable; +use Psr\Container\ContainerExceptionInterface; +use Psr\Http\Message\ServerRequestInterface; use function array_merge; use function is_array; +use function is_iterable; +use function is_string; use function sprintf; /** * Simple route stack implementation. * - * @template TRoute of RouteInterface - * @template-implements RouteStackInterface + * @extends AbstractRouteStack */ -class SimpleRouteStack implements RouteStackInterface +class SimpleRouteStack extends AbstractRouteStack { - /** - * Stack containing all routes. - * - * @var PriorityList - */ - protected $routes; - - /** - * Route plugin manager - * - * @var RoutePluginManager - */ - protected $routePluginManager; - - /** - * Default parameters. - * - * @var array - */ - protected $defaultParams = []; + /** @var SimpleRouteContainerInterface */ + protected RouteContainerInterface $routes; - /** - * @param RoutePluginManager|null $routePluginManager - */ - public function __construct(?RoutePluginManager $routePluginManager = null) - { - /** @var PriorityList $this->routes */ - $this->routes = new PriorityList(); - /** @var RoutePluginManager $this->routePluginManager */ + public function __construct( + ?RoutePluginManager $routePluginManager = null, + ?SimpleRouteContainerInterface $routes = null + ) { $this->routePluginManager = $routePluginManager ?? new RoutePluginManager(new ServiceManager()); - + $this->routes = $routes ?? new SimpleRouteContainer(); $this->init(); } /** - * factory(): defined by RouteInterface interface. - * - * @see \Laminas\Router\RouteInterface::factory() - * - * @param iterable $options - * @return SimpleRouteStack + * @inheritDoc * @throws Exception\InvalidArgumentException + * @throws ContainerExceptionInterface */ - public static function factory($options = []) + public static function factory(iterable $options = []): RouteStackInterface { - if ($options instanceof Traversable) { - $options = ArrayUtils::iteratorToArray($options); - } elseif (! is_array($options)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects an array or Traversable set of options', - __METHOD__ - )); + if (! is_array($options)) { + $options = self::iteratorToArray($options); } - $routePluginManager = null; - if (isset($options['route_plugins'])) { - $routePluginManager = $options['route_plugins']; + $routePluginManager = $options['route_plugins'] ?? null; + if ($routePluginManager !== null && ! $routePluginManager instanceof RoutePluginManager) { + throw new Exception\InvalidArgumentException('route_plugins must be an instance of RoutePluginManager'); } $instance = new static($routePluginManager); - if (isset($options['routes'])) { + if (is_iterable($options['routes'] ?? null)) { $instance->addRoutes($options['routes']); } - if (isset($options['default_params'])) { - $instance->setDefaultParams($options['default_params']); + if (is_array($options['default_params'] ?? null)) { + $defaultParams = (array) $options['default_params']; + $instance->setDefaultParams($defaultParams); } return $instance; } - /** - * Init method for extending classes. - * - * @return void - */ - protected function init() - { - } - - /** - * @param RoutePluginManager $routePlugins - * @return $this - */ - public function setRoutePluginManager(RoutePluginManager $routePlugins) - { - $this->routePluginManager = $routePlugins; - return $this; - } - - /** - * Get the route plugin manager. - * - * @return RoutePluginManager - */ - public function getRoutePluginManager() - { - return $this->routePluginManager; - } - - /** @inheritDoc */ - public function addRoutes($routes) - { - if (! is_array($routes) && ! $routes instanceof Traversable) { - throw new Exception\InvalidArgumentException('addRoutes expects an array or Traversable set of routes'); - } - - foreach ($routes as $name => $route) { - $this->addRoute($name, $route); - } - - return $this; - } - - /** @inheritDoc */ - public function addRoute($name, $route, $priority = null) - { - if (! $route instanceof RouteInterface) { - $route = $this->routeFromArray($route); - } - - if ($priority === null && isset($route->priority)) { - $priority = $route->priority; - } - - $this->routes->insert($name, $route, $priority); - - return $this; - } - - /** @inheritDoc */ - public function removeRoute($name) - { - $this->routes->remove($name); - return $this; - } - /** @inheritDoc */ - public function setRoutes($routes) - { - $this->routes->clear(); - $this->addRoutes($routes); - return $this; - } - - /** - * Get the added routes - * - * @return Traversable list of all routes - */ - public function getRoutes() - { - return $this->routes; - } - - /** - * Check if a route with a specific name exists - * - * @param string $name - * @return bool true if route exists - */ - public function hasRoute($name) - { - return $this->routes->get($name) !== null; - } - - /** - * Get a route by name - * - * @param string $name - * @return TRoute|null the route - */ - public function getRoute($name) - { - return $this->routes->get($name); - } - - /** - * Set a default parameters. - * - * @return SimpleRouteStack - */ - public function setDefaultParams(array $params) - { - $this->defaultParams = $params; - return $this; - } - - /** - * Set a default parameter. - * - * @param string $name - * @param mixed $value - * @return SimpleRouteStack - */ - public function setDefaultParam($name, $value) - { - $this->defaultParams[$name] = $value; - return $this; - } - - /** - * Create a route from array specifications. - * - * @param iterable $specs - * @return TRoute - * @throws Exception\InvalidArgumentException - */ - protected function routeFromArray($specs) - { - if ($specs instanceof Traversable) { - $specs = ArrayUtils::iteratorToArray($specs); - } - - if (! is_array($specs)) { - throw new Exception\InvalidArgumentException('Route definition must be an array or Traversable object'); - } - - if (! isset($specs['type'])) { - throw new Exception\InvalidArgumentException('Missing "type" option'); - } - - if (! isset($specs['options'])) { - $specs['options'] = []; - } - - $route = $this->getRoutePluginManager()->get($specs['type'], $specs['options']); - - if (isset($specs['priority'])) { - $route->priority = $specs['priority']; - } - - return $route; - } - - /** - * match(): defined by RouteInterface interface. - * - * @see \Laminas\Router\RouteInterface::match() - * - * @return RouteMatch|null - */ - public function match(Request $request) + public function match(ServerRequestInterface $request): ?RouteMatch { foreach ($this->routes as $name => $route) { if (($match = $route->match($request)) instanceof RouteMatch) { @@ -284,24 +87,21 @@ public function match(Request $request) } /** - * assemble(): defined by RouteInterface interface. - * - * @see \Laminas\Router\RouteInterface::assemble() - * - * @return mixed + * @inheritDoc * @throws Exception\InvalidArgumentException * @throws Exception\RuntimeException */ - public function assemble(array $params = [], array $options = []) + public function assemble(array $params = [], array $options = []): mixed { - if (! isset($options['name'])) { + $name = $options['name'] ?? null; + if (! is_string($name)) { throw new Exception\InvalidArgumentException('Missing "name" option'); } - $route = $this->routes->get($options['name']); + $route = $this->routes->get($name); if (! $route) { - throw new Exception\RuntimeException(sprintf('Route with name "%s" not found', $options['name'])); + throw new Exception\RuntimeException(sprintf('Route with name "%s" not found', $name)); } unset($options['name']); diff --git a/test/FactoryTester.php b/test/FactoryTester.php index 26467b25..eff36b57 100644 --- a/test/FactoryTester.php +++ b/test/FactoryTester.php @@ -6,6 +6,7 @@ use ArrayIterator; use Laminas\Router\Exception\InvalidArgumentException; +use Laminas\Router\RouteInterface; use PHPUnit\Framework\TestCase; use function sprintf; @@ -17,10 +18,8 @@ final class FactoryTester { /** * Test case to call assertions to. - * - * @var TestCase */ - protected $testCase; + protected TestCase $testCase; /** * Create a new factory tester. @@ -32,25 +31,11 @@ public function __construct(TestCase $testCase) /** * Test a factory. - * - * @param string $classname - * @return void */ - public function testFactory($classname, array $requiredOptions, array $options) + public function testFactory(string $classname, array $requiredOptions, array $options): void { $factory = sprintf('%s::factory', $classname); - // Test that the factory does not allow a scalar option. - try { - $factory(0); - $this->testCase->fail('An expected exception was not thrown'); - } catch (InvalidArgumentException $e) { - $this->testCase->assertStringContainsString( - 'factory expects an array or Traversable set of options', - $e->getMessage() - ); - } - // Test required options. foreach ($requiredOptions as $option => $exceptionMessage) { $testOptions = $options; @@ -66,9 +51,11 @@ public function testFactory($classname, array $requiredOptions, array $options) } // Create the route, will throw an exception if something goes wrong. - $factory($options); + $route = $factory($options); + $this->testCase->assertInstanceOf(RouteInterface::class, $route); // Try the same with an iterator. - $factory(new ArrayIterator($options)); + $route = $factory(new ArrayIterator($options)); + $this->testCase->assertInstanceOf(RouteInterface::class, $route); } } diff --git a/test/Http/ChainTest.php b/test/Http/ChainTest.php index 74198315..2e229b82 100644 --- a/test/Http/ChainTest.php +++ b/test/Http/ChainTest.php @@ -4,17 +4,18 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\Router\Http\Chain; use Laminas\Router\Http\HttpRouteInterface; use Laminas\Router\Http\RouteMatch; use Laminas\Router\Http\Segment; -use Laminas\Router\Http\Wildcard; use Laminas\Router\RoutePluginManager; use Laminas\ServiceManager\ServiceManager; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; use function strlen; use function strpos; @@ -46,9 +47,6 @@ public static function getRoute(): Chain ], ], ], - [ - 'type' => Wildcard::class, - ], ], $routePlugins ); @@ -144,15 +142,13 @@ public static function routeProvider(): array } /** - * @param string $path - * @param int|null $offset + * @throws ContainerExceptionInterface */ #[DataProvider('routeProvider')] - public function testMatching(Chain $route, $path, $offset, ?array $params = null) + public function testMatching(Chain $route, string $path, ?int $offset, ?array $params = null): void { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset); + $request = new MockServerRequest(new MockUri('https://example.com' . $path)); + $match = $route->match($request, $offset); if ($params === null) { $this->assertNull($match); @@ -169,12 +165,8 @@ public function testMatching(Chain $route, $path, $offset, ?array $params = null } } - /** - * @param string $path - * @param int|null $offset - */ #[DataProvider('routeProvider')] - public function testAssembling(Chain $route, $path, $offset, ?array $params = null) + public function testAssembling(Chain $route, string $path, ?int $offset, ?array $params = null): void { if ($params === null) { // Data which will not match are not tested for assembling. @@ -184,20 +176,20 @@ public function testAssembling(Chain $route, $path, $offset, ?array $params = nu $result = $route->assemble($params); if ($offset !== null) { - $this->assertEquals($offset, strpos($path, (string) $result, $offset)); + $this->assertEquals($offset, strpos($path, $result, $offset)); } else { $this->assertEquals($path, $result); } } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( Chain::class, [ - 'routes' => 'Missing "routes" in options array', - 'route_plugins' => 'Missing "route_plugins" in options array', + 'routes' => 'Missing "routes" option', + 'route_plugins' => 'Missing "route_plugins" option', ], [ 'routes' => [], diff --git a/test/Http/HostnameTest.php b/test/Http/HostnameTest.php index 4de9d999..feebcfb5 100644 --- a/test/Http/HostnameTest.php +++ b/test/Http/HostnameTest.php @@ -4,14 +4,14 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\Router\Exception\InvalidArgumentException; use Laminas\Router\Exception\RuntimeException; use Laminas\Router\Http\Hostname; use Laminas\Router\Http\RouteMatch; -use Laminas\Stdlib\Request as BaseRequest; use Laminas\Uri\Http as HttpUri; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; @@ -90,7 +90,7 @@ public static function routeProvider(): array 'one-of-two-missing-optional-subdomain' => [ new Hostname('[:foo.][:bar.]example.com'), 'bat.example.com', - ['foo' => null, 'foo' => 'bat'], + ['foo' => null, 'bar' => 'bat'], ], 'two-missing-optional-subdomain' => [ new Hostname('[:foo.][:bar.]example.com'), @@ -165,15 +165,11 @@ public static function routeProvider(): array ]; } - /** - * @param string $hostname - */ #[DataProvider('routeProvider')] - public function testMatching(Hostname $route, $hostname, ?array $params = null) + public function testMatching(Hostname $route, string $hostname, ?array $params = null): void { - $request = new Request(); - $request->setUri('http://' . $hostname . '/'); - $match = $route->match($request); + $request = new MockServerRequest(new MockUri('https://' . $hostname . '/')); + $match = $route->match($request); if ($params === null) { $this->assertNull($match); @@ -186,14 +182,10 @@ public function testMatching(Hostname $route, $hostname, ?array $params = null) } } - /** - * @param string $hostname - */ #[DataProvider('routeProvider')] - public function testAssembling(Hostname $route, $hostname, ?array $params = null) + public function testAssembling(Hostname $route, string $hostname, ?array $params = null): void { if ($params === null) { - // Data which will not match are not tested for assembling. $this->expectNotToPerformAssertions(); return; } @@ -205,19 +197,10 @@ public function testAssembling(Hostname $route, $hostname, ?array $params = null $this->assertEquals($hostname, $uri->getHost()); } - public function testNoMatchWithoutUriMethod() - { - $route = new Hostname('example.com'); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); - } - public function testNoMatchWithRelativeUri(): void { $route = new Hostname('example.com'); - $request = new Request(); - $request->setUri('/relative/path'); + $request = new MockServerRequest(new MockUri('/relative/path')); self::assertNull($route->match($request)); } @@ -225,8 +208,7 @@ public function testNoMatchWithRelativeUri(): void public function testNoMatchWithPlaceholderOnRelativeUri(): void { $route = new Hostname(':domain'); - $request = new Request(); - $request->setUri('/relative/path'); + $request = new MockServerRequest(new MockUri('/relative/path')); self::assertNull($route->match($request)); } @@ -234,15 +216,14 @@ public function testNoMatchWithPlaceholderOnRelativeUri(): void public function testMatchesRelativeUriWithFullyOptionalDefinition(): void { $route = new Hostname('[:domain]'); - $request = new Request(); - $request->setUri('/relative/path'); + $request = new MockServerRequest(new MockUri('/relative/path')); $match = $route->match($request); self::assertInstanceOf(RouteMatch::class, $match); self::assertArrayNotHasKey('domain', $match->getParams()); } - public function testAssemblingWithMissingParameter() + public function testAssemblingWithMissingParameter(): void { $route = new Hostname(':foo.example.com'); $uri = new HttpUri(); @@ -252,7 +233,7 @@ public function testAssemblingWithMissingParameter() $route->assemble([], ['uri' => $uri]); } - public function testGetAssembledParams() + public function testGetAssembledParams(): void { $route = new Hostname(':foo.example.com'); $uri = new HttpUri(); @@ -261,13 +242,13 @@ public function testGetAssembledParams() $this->assertEquals(['foo'], $route->getAssembledParams()); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( Hostname::class, [ - 'route' => 'Missing "route" in options array', + 'route' => 'Missing "route" option', ], [ 'route' => 'example.com', @@ -276,7 +257,7 @@ public function testFactory() } #[Group('laminas5656')] - public function testFailedHostnameSegmentMatchDoesNotEmitErrors() + public function testFailedHostnameSegmentMatchDoesNotEmitErrors(): void { $this->expectException(RuntimeException::class); new Hostname(':subdomain.with_underscore.com'); diff --git a/test/Http/LiteralTest.php b/test/Http/LiteralTest.php index 28a8f3d5..c2c30b0a 100644 --- a/test/Http/LiteralTest.php +++ b/test/Http/LiteralTest.php @@ -4,11 +4,11 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\Router\Http\Literal; use Laminas\Router\Http\RouteMatch; -use Laminas\Stdlib\Request as BaseRequest; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; @@ -62,17 +62,11 @@ public static function routeProvider(): array ]; } - /** - * @param string $path - * @param int|null $offset - * @param bool $shouldMatch - */ #[DataProvider('routeProvider')] - public function testMatching(Literal $route, $path, $offset, $shouldMatch) + public function testMatching(Literal $route, string $path, ?int $offset, bool $shouldMatch): void { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset); + $request = new MockServerRequest(new MockUri('http://example.com' . $path)); + $match = $route->match($request, $offset); if (! $shouldMatch) { $this->assertNull($match); @@ -85,16 +79,10 @@ public function testMatching(Literal $route, $path, $offset, $shouldMatch) } } - /** - * @param string $path - * @param int|null $offset - * @param bool $shouldMatch - */ #[DataProvider('routeProvider')] - public function testAssembling(Literal $route, $path, $offset, $shouldMatch) + public function testAssembling(Literal $route, string $path, ?int $offset, bool $shouldMatch): void { if (! $shouldMatch) { - // Data which will not match are not tested for assembling. $this->expectNotToPerformAssertions(); return; } @@ -102,35 +90,28 @@ public function testAssembling(Literal $route, $path, $offset, $shouldMatch) $result = $route->assemble(); if ($offset !== null) { - $this->assertEquals($offset, strpos($path, (string) $result, $offset)); + $this->assertEquals($offset, strpos($path, $result, $offset)); } else { $this->assertEquals($path, $result); } } - public function testNoMatchWithoutUriMethod() - { - $route = new Literal('/foo'); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); - } - - public function testGetAssembledParams() + public function testGetAssembledParams(): void { - $route = new Literal('/foo'); - $route->assemble(['foo' => 'bar']); + $route = new Literal('/foo'); + $result = $route->assemble(['foo' => 'bar']); + $this->assertEquals('/foo', $result); $this->assertEquals([], $route->getAssembledParams()); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( Literal::class, [ - 'route' => 'Missing "route" in options array', + 'route' => 'Missing "route" option', ], [ 'route' => '/foo', @@ -139,9 +120,9 @@ public function testFactory() } #[Group('Laminas-436')] - public function testEmptyLiteral() + public function testEmptyLiteral(): void { - $request = new Request(); + $request = new MockServerRequest(); $route = new Literal(''); $this->assertNull($route->match($request, 0)); } diff --git a/test/Http/MethodTest.php b/test/Http/MethodTest.php index d8225ec3..926eab91 100644 --- a/test/Http/MethodTest.php +++ b/test/Http/MethodTest.php @@ -4,11 +4,11 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\Router\Http\Method as HttpMethod; use Laminas\Router\Http\RouteMatch; -use Laminas\Stdlib\Request as BaseRequest; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -25,52 +25,39 @@ public static function routeProvider(): array return [ 'simple-match' => [ new HttpMethod('get'), - 'get', + 'GET', ], 'match-comma-separated-verbs' => [ new HttpMethod('get,post'), - 'get', + 'GET', ], 'match-comma-separated-verbs-ws' => [ new HttpMethod('get , post , put'), - 'post', + 'POST', ], 'match-ignores-case' => [ new HttpMethod('Get'), - 'get', + 'GET', ], ]; } - /** - * @param string $verb - */ #[DataProvider('routeProvider')] - public function testMatching(HttpMethod $route, $verb) + public function testMatching(HttpMethod $route, string $verb): void { - $request = new Request(); - $request->setUri('http://example.com'); - $request->setMethod($verb); + $request = new MockServerRequest(new MockUri('http://example.com'), $verb); $match = $route->match($request); $this->assertInstanceOf(RouteMatch::class, $match); } - public function testNoMatchWithoutVerb() - { - $route = new HttpMethod('get'); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); - } - - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( HttpMethod::class, [ - 'verb' => 'Missing "verb" in options array', + 'verb' => 'Missing "verb" option', ], [ 'verb' => 'get', diff --git a/test/Http/PartTest.php b/test/Http/PartTest.php index bcb5aeaa..0024e26f 100644 --- a/test/Http/PartTest.php +++ b/test/Http/PartTest.php @@ -5,7 +5,6 @@ namespace LaminasTest\Router\Http; use ArrayObject; -use Laminas\Http\Request; use Laminas\Router\Exception\InvalidArgumentException; use Laminas\Router\Exception\RuntimeException; use Laminas\Router\Http\HttpRouteInterface; @@ -13,16 +12,16 @@ use Laminas\Router\Http\Part; use Laminas\Router\Http\RouteMatch; use Laminas\Router\Http\Segment; -use Laminas\Router\Http\Wildcard; use Laminas\Router\RouteInvokableFactory; use Laminas\Router\RoutePluginManager; use Laminas\ServiceManager\ServiceManager; -use Laminas\Stdlib\Parameters; -use Laminas\Stdlib\Request as BaseRequest; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; use function strlen; use function strpos; @@ -34,32 +33,24 @@ public static function getRoutePlugins(): RoutePluginManager { return new RoutePluginManager(new ServiceManager(), [ 'aliases' => [ - 'literal' => Literal::class, - 'Literal' => Literal::class, - 'part' => Part::class, - 'Part' => Part::class, - 'segment' => Segment::class, - 'Segment' => Segment::class, - 'wildcard' => Wildcard::class, - 'Wildcard' => Wildcard::class, - 'wildCard' => Wildcard::class, - 'WildCard' => Wildcard::class, + 'literal' => Literal::class, + 'Literal' => Literal::class, + 'part' => Part::class, + 'Part' => Part::class, + 'segment' => Segment::class, + 'Segment' => Segment::class, ], 'factories' => [ - Literal::class => RouteInvokableFactory::class, - Part::class => RouteInvokableFactory::class, - Segment::class => RouteInvokableFactory::class, - Wildcard::class => RouteInvokableFactory::class, - - // v2 normalized names - 'laminasmvcrouterhttpliteral' => RouteInvokableFactory::class, - 'laminasmvcrouterhttppart' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpsegment' => RouteInvokableFactory::class, - 'laminasmvcrouterhttpwildcard' => RouteInvokableFactory::class, + Literal::class => RouteInvokableFactory::class, + Part::class => RouteInvokableFactory::class, + Segment::class => RouteInvokableFactory::class, ], ]); } + /** + * @throws ContainerExceptionInterface + */ public static function getRoute(): Part { return new Part( @@ -96,11 +87,6 @@ public static function getRoute(): Part 'route' => '/:controller', ], 'may_terminate' => true, - 'child_routes' => [ - 'wildcard' => [ - 'type' => Wildcard::class, - ], - ], ], ], ], @@ -132,33 +118,6 @@ public static function getRoute(): Part ); } - public static function getRouteAlternative(): Part - { - return new Part( - [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/[:controller[/:action]]', - 'defaults' => [ - 'controller' => 'fo-fo', - 'action' => 'index', - ], - ], - ], - true, - self::getRoutePlugins(), - [ - 'wildcard' => [ - 'type' => Wildcard::class, - 'options' => [ - 'key_value_delimiter' => '/', - 'param_delimiter' => '/', - ], - ], - ] - ); - } - /** * @psalm-return array 'bat'], ], - 'parameters-are-used-only-once' => [ - self::getRoute(), - '/foo/baz/wildcard/foo/bar', - null, - 'baz/bat/wildcard', - ['controller' => 'wildcard', 'foo' => 'bar'], - ], 'optional-parameters-are-dropped-without-child' => [ self::getRoute(), '/foo/bat', @@ -248,53 +200,19 @@ public static function routeProvider(): array 'bat/optional', ['foo' => 'bar'], ], - 'simple-match' => [ - self::getRouteAlternative(), - '/', - null, - null, - [ - 'controller' => 'fo-fo', - 'action' => 'index', - ], - ], - 'match-wildcard' => [ - self::getRouteAlternative(), - '/fo-fo/index/param1/value1', - null, - 'wildcard', - [ - 'controller' => 'fo-fo', - 'action' => 'index', - 'param1' => 'value1', - ], - ], - /* - 'match-query' => array( - self::getRouteAlternative(), - '/fo-fo/index?param1=value1', - 0, - 'query', - array( - 'controller' => 'fo-fo', - 'action' => 'index' - ) - ) - */ ]; } - /** - * @param string $path - * @param int|null $offset - * @param string $routeName - */ #[DataProvider('routeProvider')] - public function testMatching(Part $route, $path, $offset, $routeName, ?array $params = null) - { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset); + public function testMatching( + Part $route, + string $path, + ?int $offset, + ?string $routeName, + ?array $params = null + ): void { + $request = new MockServerRequest(new MockUri('http://example.com' . $path)); + $match = $route->match($request, $offset); if ($params === null) { $this->assertNull($match); @@ -313,17 +231,13 @@ public function testMatching(Part $route, $path, $offset, $routeName, ?array $pa } } - /** - * @param string $path - * @param int|null $offset - * @param string $routeName - */ #[DataProvider('routeProvider')] - public function testAssembling(Part $route, $path, $offset, $routeName, ?array $params = null) + public function testAssembling(Part $route, string $path, ?int $offset, ?string $routeName, ?array $params = null) { if ($params === null) { // Data which will not match are not tested for assembling. $this->expectNotToPerformAssertions(); + return; } @@ -336,14 +250,17 @@ public function testAssembling(Part $route, $path, $offset, $routeName, ?array $ } } - public function testAssembleNonTerminatedRoute() + public function testAssembleNonTerminatedRoute(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Part route may not terminate'); self::getRoute()->assemble([], ['name' => 'baz']); } - public function testBaseRouteMayNotBePartRoute() + /** + * @throws ContainerExceptionInterface + */ + public function testBaseRouteMayNotBePartRoute(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Base route may not be a part route'); @@ -351,15 +268,7 @@ public function testBaseRouteMayNotBePartRoute() new Part(self::getRoute(), true, new RoutePluginManager(new ServiceManager())); } - public function testNoMatchWithoutUriMethod() - { - $route = self::getRoute(); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); - } - - public function testGetAssembledParams() + public function testGetAssembledParams(): void { $route = self::getRoute(); $route->assemble(['controller' => 'foo'], ['name' => 'baz/bat']); @@ -367,14 +276,14 @@ public function testGetAssembledParams() $this->assertEquals([], $route->getAssembledParams()); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( Part::class, [ - 'route' => 'Missing "route" in options array', - 'route_plugins' => 'Missing "route_plugins" in options array', + 'route' => 'Missing "route" option', + 'route_plugins' => 'Missing "route_plugins" option', ], [ 'route' => new Literal('/foo'), @@ -384,7 +293,7 @@ public function testFactory() } #[Group('Laminas-105')] - public function testFactoryShouldAcceptTraversableChildRoutes() + public function testFactoryShouldAcceptTraversableChildRoutes(): void { $children = new ArrayObject([ 'create' => [ @@ -404,7 +313,7 @@ public function testFactoryShouldAcceptTraversableChildRoutes() 'options' => [ 'route' => '/admin/users', 'defaults' => [ - 'controller' => 'Admin\UserController', + 'controller' => 'Admin\\UserController', 'action' => 'index', ], ], @@ -419,7 +328,7 @@ public function testFactoryShouldAcceptTraversableChildRoutes() } #[Group('3711')] - public function testPartRouteMarkedAsMayTerminateCanMatchWhenQueryStringPresent() + public function testPartRouteMarkedAsMayTerminateCanMatchWhenQueryStringPresent(): void { $options = [ 'route' => [ @@ -448,49 +357,10 @@ public function testPartRouteMarkedAsMayTerminateCanMatchWhenQueryStringPresent( ]; $route = Part::factory($options); - $request = new Request(); - $request->setUri('http://example.com/resource?foo=bar'); - $query = new Parameters(['foo' => 'bar']); - $request->setQuery($query); - $query = $request->getQuery(); + $request = new MockServerRequest(new MockUri('http://example.com/resource?foo=bar')); $match = $route->match($request); $this->assertInstanceOf(\Laminas\Router\RouteMatch::class, $match); $this->assertEquals('resource', $match->getParam('action')); } - - #[Group('3711')] - public function testPartRouteMarkedAsMayTerminateButWithQueryRouteChildWillMatchChildRoute() - { - $options = [ - 'route' => [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/resource', - 'defaults' => [ - 'controller' => 'ResourceController', - 'action' => 'resource', - ], - ], - ], - 'route_plugins' => self::getRoutePlugins(), - 'may_terminate' => true, - ]; - - $route = Part::factory($options); - $request = new Request(); - $request->setUri('http://example.com/resource?foo=bar'); - $query = new Parameters(['foo' => 'bar']); - $request->setQuery($query); - $query = $request->getQuery(); - - /** @link https://github.com/laminas/laminas-router/commit/66ebd439067d9e25a6f7941de4b9ebc9c52524f5 */ - $this->markTestSkipped('This test fails and has been skipped because the Query route has been deprecated (?)'); - - /* - $match = $route->match($request); - $this->assertInstanceOf(\Laminas\Router\RouteMatch::class, $match); - $this->assertEquals('string', $match->getParam('query')); - */ - } } diff --git a/test/Http/PlaceholderTest.php b/test/Http/PlaceholderTest.php index ecc2bdc2..b379265e 100644 --- a/test/Http/PlaceholderTest.php +++ b/test/Http/PlaceholderTest.php @@ -4,19 +4,33 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\Router\Http\Hostname; use Laminas\Router\Http\Literal; use Laminas\Router\Http\Placeholder; use Laminas\Router\Http\RouteMatch; use Laminas\Router\Http\TreeRouteStack; +use Laminas\Router\RouteInterface; +use Laminas\Router\RoutePluginManager; +use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\ArrayUtils; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; final class PlaceholderTest extends TestCase { + private function createRoutePluginManager(): RoutePluginManager + { + return new RoutePluginManager(new ServiceManager(), [ + 'invokables' => [ + Placeholder::class => Placeholder::class, + ], + ]); + } + /** @var array> */ private static array $routeConfig = [ 'auth' => [ @@ -45,49 +59,47 @@ final class PlaceholderTest extends TestCase ], ], ]; - public function testMatch() + public function testMatch(): void { $route = new Placeholder([]); - $request = new Request(); - $request->setUri('http://example.com/'); - $match = $route->match($request); + $request = new MockServerRequest(new MockUri('https://example.com/')); + $match = $route->match($request); $this->assertInstanceOf(RouteMatch::class, $match); } - public function testAssembling() + public function testAssembling(): void { $route = new Placeholder([]); $this->assertEquals('', $route->assemble()); } - public function testGetAssembledParams() + public function testGetAssembledParams(): void { $route = new Placeholder([]); $this->assertEquals([], $route->getAssembledParams()); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory(Placeholder::class, [], []); } /** - * @param array $additionalConfig - * @param string $uri - * @param string $expectedRouteName + * @throws ContainerExceptionInterface */ #[DataProvider('placeholderProvider')] - public function testPlaceholderDefault($additionalConfig, $uri, $expectedRouteName) + public function testPlaceholderDefault(array $additionalConfig, string $uri, string $expectedRouteName): void { + /** @var iterable $routeConfig */ $routeConfig = ArrayUtils::merge(self::$routeConfig, $additionalConfig); - $router = TreeRouteStack::factory(['routes' => $routeConfig]); + $router = new TreeRouteStack($this->createRoutePluginManager()); + $router->addRoutes($routeConfig); - $request = new Request(); - $request->setUri($uri); - $match = $router->match($request); + $request = new MockServerRequest(new MockUri($uri)); + $match = $router->match($request); $this->assertInstanceOf(RouteMatch::class, $match); $this->assertEquals($expectedRouteName, $match->getMatchedRouteName()); diff --git a/test/Http/RegexTest.php b/test/Http/RegexTest.php index 5f263a47..05cf6f3b 100644 --- a/test/Http/RegexTest.php +++ b/test/Http/RegexTest.php @@ -4,11 +4,11 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\Router\Http\Regex; use Laminas\Router\Http\RouteMatch; -use Laminas\Stdlib\Request as BaseRequest; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -79,16 +79,11 @@ public static function routeProvider(): array ]; } - /** - * @param string $path - * @param int|null $offset - */ #[DataProvider('routeProvider')] - public function testMatching(Regex $route, $path, $offset, ?array $params = null) + public function testMatching(Regex $route, string $path, ?int $offset, ?array $params = null): void { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset); + $request = new MockServerRequest(new MockUri('http://example.com' . $path)); + $match = $route->match($request, $offset); if ($params === null) { $this->assertNull($match); @@ -105,12 +100,8 @@ public function testMatching(Regex $route, $path, $offset, ?array $params = null } } - /** - * @param string $path - * @param int|null $offset - */ #[DataProvider('routeProvider')] - public function testAssembling(Regex $route, $path, $offset, ?array $params = null) + public function testAssembling(Regex $route, string $path, ?int $offset, ?array $params = null): void { if ($params === null) { // Data which will not match are not tested for assembling. @@ -127,15 +118,7 @@ public function testAssembling(Regex $route, $path, $offset, ?array $params = nu } } - public function testNoMatchWithoutUriMethod() - { - $route = new Regex('/foo', '/foo'); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); - } - - public function testGetAssembledParams() + public function testGetAssembledParams(): void { $route = new Regex('/(?.+)', '/%foo%'); $route->assemble(['foo' => 'bar', 'baz' => 'bat']); @@ -143,14 +126,14 @@ public function testGetAssembledParams() $this->assertEquals(['foo'], $route->getAssembledParams()); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( Regex::class, [ - 'regex' => 'Missing "regex" in options array', - 'spec' => 'Missing "spec" in options array', + 'regex' => 'Missing "regex" option', + 'spec' => 'Missing "spec" option', ], [ 'regex' => '/foo', @@ -159,20 +142,19 @@ public function testFactory() ); } - public function testRawDecode() + public function testRawDecode(): void { // verify all characters which don't absolutely require encoding pass through match unchanged // this includes every character other than #, %, / and ? $raw = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`-=[]\\;\',.~!@$^&*()_+{}|:"<>'; - $request = new Request(); - $request->setUri('http://example.com/' . $raw); - $route = new Regex('/(?[^/]+)', '/%foo%'); - $match = $route->match($request); + $request = new MockServerRequest(new MockUri('http://example.com/' . $raw)); + $route = new Regex('/(?[^/]+)', '/%foo%'); + $match = $route->match($request); $this->assertSame($raw, $match->getParam('foo')); } - public function testEncodedDecode() + public function testEncodedDecode(): void { // @codingStandardsIgnoreStart // every character @@ -180,10 +162,9 @@ public function testEncodedDecode() $out = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`-=[]\\;\',./~!@#$%^&*()_+{}|:"<>?'; // @codingStandardsIgnoreEnd - $request = new Request(); - $request->setUri('http://example.com/' . $in); - $route = new Regex('/(?[^/]+)', '/%foo%'); - $match = $route->match($request); + $request = new MockServerRequest(new MockUri('http://example.com/' . $in)); + $route = new Regex('/(?[^/]+)', '/%foo%'); + $match = $route->match($request); $this->assertSame($out, $match->getParam('foo')); } diff --git a/test/Http/SchemeTest.php b/test/Http/SchemeTest.php index bab9f8c1..b7915ef3 100644 --- a/test/Http/SchemeTest.php +++ b/test/Http/SchemeTest.php @@ -4,20 +4,19 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\Router\Http\RouteMatch; use Laminas\Router\Http\Scheme; -use Laminas\Stdlib\Request as BaseRequest; use Laminas\Uri\Http as HttpUri; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\TestCase; final class SchemeTest extends TestCase { - public function testMatching() + public function testMatching(): void { - $request = new Request(); - $request->setUri('https://example.com/'); + $request = new MockServerRequest(new MockUri('https://example.com/')); $route = new Scheme('https'); $match = $route->match($request); @@ -25,10 +24,9 @@ public function testMatching() $this->assertInstanceOf(RouteMatch::class, $match); } - public function testNoMatchingOnDifferentScheme() + public function testNoMatchingOnDifferentScheme(): void { - $request = new Request(); - $request->setUri('http://example.com/'); + $request = new MockServerRequest(new MockUri('http://example.com/')); $route = new Scheme('https'); $match = $route->match($request); @@ -46,15 +44,7 @@ public function testAssembling() $this->assertEquals('https', $uri->getScheme()); } - public function testNoMatchWithoutUriMethod() - { - $route = new Scheme('https'); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); - } - - public function testGetAssembledParams() + public function testGetAssembledParams(): void { $route = new Scheme('https'); $route->assemble(['foo' => 'bar']); @@ -62,13 +52,13 @@ public function testGetAssembledParams() $this->assertEquals([], $route->getAssembledParams()); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( Scheme::class, [ - 'scheme' => 'Missing "scheme" in options array', + 'scheme' => 'Missing "scheme" option', ], [ 'scheme' => 'http', diff --git a/test/Http/SegmentTest.php b/test/Http/SegmentTest.php index 6509d321..cdc59014 100644 --- a/test/Http/SegmentTest.php +++ b/test/Http/SegmentTest.php @@ -4,7 +4,6 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\I18n\Translator\Loader\FileLoaderInterface; use Laminas\I18n\Translator\TextDomain; use Laminas\I18n\Translator\Translator; @@ -12,11 +11,14 @@ use Laminas\Router\Exception\RuntimeException; use Laminas\Router\Http\RouteMatch; use Laminas\Router\Http\Segment; -use Laminas\Stdlib\Request as BaseRequest; +use Laminas\Validator\Translator\TranslatorAwareInterface; use LaminasTest\Router\FactoryTester; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use function class_exists; use function implode; use function strlen; use function strpos; @@ -183,6 +185,10 @@ public static function routeProvider(): array public function testL10nRoute(): void { + if (! class_exists(TranslatorAwareInterface::class)) { + $this->markTestSkipped('laminas-i18n is not installed'); + } + $translator = new Translator(); $translator->setLocale('en-US'); @@ -204,28 +210,24 @@ public function testL10nRoute(): void $this->matchingWithL10n( new Segment('/{fw}', [], []), '/framework', - null, [], ['translator' => $translator] ); $this->matchingWithL10n( new Segment('/{fw}', [], []), '/baukasten', - null, [], ['translator' => $translator, 'locale' => 'de-DE'] ); $this->matchingWithL10n( new Segment('/{fw}', [], []), '/fw', - null, [], ['translator' => $translator, 'locale' => 'fr-FR'] ); $this->matchingWithL10n( new Segment('/{fw}', [], []), '/fw-alternative', - null, [], ['translator' => $translator, 'text_domain' => 'alternative'] ); @@ -233,28 +235,24 @@ public function testL10nRoute(): void $this->assemblingWithL10n( new Segment('/{fw}', [], []), '/framework', - null, [], ['translator' => $translator] ); $this->assemblingWithL10n( new Segment('/{fw}', [], []), '/baukasten', - null, [], ['translator' => $translator, 'locale' => 'de-DE'] ); $this->assemblingWithL10n( new Segment('/{fw}', [], []), '/fw', - null, [], ['translator' => $translator, 'locale' => 'fr-FR'] ); $this->assemblingWithL10n( new Segment('/{fw}', [], []), '/fw-alternative', - null, [], ['translator' => $translator, 'text_domain' => 'alternative'] ); @@ -287,9 +285,6 @@ public static function parseExceptionsProvider(): array ]; } - /** - * @param array|null $params - */ #[DataProvider('routeProvider')] public function testMatching( Segment $route, @@ -298,9 +293,8 @@ public function testMatching( ?array $params = null, array $options = [] ): void { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset, $options); + $request = new MockServerRequest(new MockUri('http://example.com' . $path)); + $match = $route->match($request, $offset, $options); if ($params === null) { $this->assertNull($match); @@ -334,28 +328,27 @@ public function testAssembling( $result = $route->assemble($params, $options); if ($offset !== null) { - $this->assertEquals($offset, strpos($path, (string) $result, $offset)); + $this->assertEquals($offset, strpos($path, $result, $offset)); } else { $this->assertEquals($path, $result); } } - /** - * @param string $path - * @param int|null $offset - */ - private function matchingWithL10n(Segment $route, $path, $offset, ?array $params = null, array $options = []) - { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset, $options); + private function matchingWithL10n( + Segment $route, + string $path, + ?array $params = null, + array $options = [] + ): void { + $request = new MockServerRequest(new MockUri('http://example.com' . $path)); + $match = $route->match($request, null, $options); if ($params === null) { $this->assertNull($match); } else { $this->assertInstanceOf(RouteMatch::class, $match); - if ($offset === null) { + if (null === null) { $this->assertEquals(strlen($path), $match->getLength()); } @@ -365,40 +358,30 @@ private function matchingWithL10n(Segment $route, $path, $offset, ?array $params } } - /** - * @param string $path - * @param int|null $offset - */ - private function assemblingWithL10n(Segment $route, $path, $offset, ?array $params = null, array $options = []) - { + private function assemblingWithL10n( + Segment $route, + string $path, + ?array $params = null, + array $options = [] + ): void { if ($params === null) { // Data which will not match are not tested for assembling. return; } $result = $route->assemble($params, $options); - - if ($offset !== null) { - $this->assertEquals($offset, strpos($path, (string) $result, $offset)); - } else { - $this->assertEquals($path, $result); - } + $this->assertEquals($path, $result); } - /** - * @param string $route - * @param string $exceptionName - * @param string $exceptionMessage - */ #[DataProvider('parseExceptionsProvider')] - public function testParseExceptions($route, $exceptionName, $exceptionMessage) + public function testParseExceptions(string $route, string $exceptionName, string $exceptionMessage): void { $this->expectException($exceptionName); $this->expectExceptionMessage($exceptionMessage); new Segment($route); } - public function testAssemblingWithMissingParameterInRoot() + public function testAssemblingWithMissingParameterInRoot(): void { $route = new Segment('/:foo'); @@ -407,7 +390,7 @@ public function testAssemblingWithMissingParameterInRoot() $route->assemble(); } - public function testTranslatedAssemblingThrowsExceptionWithoutTranslator() + public function testTranslatedAssemblingThrowsExceptionWithoutTranslator(): void { $route = new Segment('/{foo}'); @@ -416,24 +399,16 @@ public function testTranslatedAssemblingThrowsExceptionWithoutTranslator() $route->assemble(); } - public function testTranslatedMatchingThrowsExceptionWithoutTranslator() + public function testTranslatedMatchingThrowsExceptionWithoutTranslator(): void { $route = new Segment('/{foo}'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('No translator provided'); - $route->match(new Request()); - } - - public function testNoMatchWithoutUriMethod() - { - $route = new Segment('/foo'); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); + $route->match(new MockServerRequest()); } - public function testAssemblingWithExistingChild() + public function testAssemblingWithExistingChild(): void { $route = new Segment('/[:foo]', [], ['foo' => 'bar']); $path = $route->assemble([], ['has_child' => true]); @@ -441,13 +416,13 @@ public function testAssemblingWithExistingChild() $this->assertEquals('/bar', $path); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( Segment::class, [ - 'route' => 'Missing "route" in options array', + 'route' => 'Missing "route" option', ], [ 'route' => '/:foo[/:bar{-}]', @@ -456,20 +431,19 @@ public function testFactory() ); } - public function testRawDecode() + public function testRawDecode(): void { // verify all characters which don't absolutely require encoding pass through match unchanged // this includes every character other than #, %, / and ? $raw = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`-=[]\\;\',.~!@$^&*()_+{}|:"<>'; - $request = new Request(); - $request->setUri('http://example.com/' . $raw); - $route = new Segment('/:foo'); - $match = $route->match($request); + $request = new MockServerRequest(new MockUri('http://example.com/' . $raw)); + $route = new Segment('/:foo'); + $match = $route->match($request); $this->assertSame($raw, $match->getParam('foo')); } - public function testEncodedDecode() + public function testEncodedDecode(): void { // @codingStandardsIgnoreStart // every character @@ -477,29 +451,27 @@ public function testEncodedDecode() $out = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`-=[]\\;\',./~!@#$%^&*()_+{}|:"<>?'; // @codingStandardsIgnoreEnd - $request = new Request(); - $request->setUri('http://example.com/' . $in); - $route = new Segment('/:foo'); - $match = $route->match($request); + $request = new MockServerRequest(new MockUri('http://example.com/' . $in)); + $route = new Segment('/:foo'); + $match = $route->match($request); $this->assertSame($out, $match->getParam('foo')); } - public function testEncodeCache() + public function testEncodeCache(): void { $params1 = ['p1' => 6.123, 'p2' => 7]; $uri1 = 'example.com/' . implode('/', $params1); $params2 = ['p1' => 6, 'p2' => 'test']; $uri2 = 'example.com/' . implode('/', $params2); - $route = new Segment('example.com/:p1/:p2'); - $request = new Request(); + $route = new Segment('example.com/:p1/:p2'); - $request->setUri($uri1); + $request = new MockServerRequest(new MockUri($uri1)); $route->match($request); $this->assertSame($uri1, $route->assemble($params1)); - $request->setUri($uri2); + $request = new MockServerRequest(new MockUri($uri2)); $route->match($request); $this->assertSame($uri2, $route->assemble($params2)); } diff --git a/test/Http/TestAsset/DummyRoute.php b/test/Http/TestAsset/DummyRoute.php index 7873ec85..2313b299 100644 --- a/test/Http/TestAsset/DummyRoute.php +++ b/test/Http/TestAsset/DummyRoute.php @@ -6,57 +6,61 @@ use Laminas\Router\Http\HttpRouteInterface; use Laminas\Router\Http\RouteMatch; -use Laminas\Stdlib\RequestInterface; +use Laminas\Router\RouteInterface; +use Laminas\Router\RoutePriorityTrait; +use Psr\Http\Message\ServerRequestInterface; + +use function strlen; /** * Dummy route. */ class DummyRoute implements HttpRouteInterface { + use RoutePriorityTrait; + /** - * match(): defined by RouteInterface interface. + * match(): defined by HttpRouteInterface interface. * * @see Route::match() - * - * @param int $pathOffset - * @return RouteMatch */ - public function match(RequestInterface $request, $pathOffset = null) - { - return new RouteMatch(['offset' => $pathOffset], -4); + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): RouteMatch { + $pathLength = strlen($request->getUri()->getPath()); + $matchLength = $pathLength - ($pathOffset ?? 0); + + return new RouteMatch(['offset' => $pathOffset], $matchLength); } /** - * assemble(): defined by RouteInterface interface. + * assemble(): defined by HttpRouteInterface interface. * * @see Route::assemble() - * - * @return mixed */ - public function assemble(?array $params = null, ?array $options = null) + public function assemble(?array $params = null, ?array $options = null): string { return ''; } /** - * factory(): defined by RouteInterface interface + * factory(): defined by HttpRouteInterface interface * - * @param iterable $options * @return DummyRoute */ - public static function factory($options = []) + public static function factory(iterable $options = []): RouteInterface { return new static(); } /** - * getAssembledParams(): defined by RouteInterface interface. + * getAssembledParams(): defined by HttpRouteInterface interface. * * @see Route::getAssembledParams - * - * @return array */ - public function getAssembledParams() + public function getAssembledParams(): array { return []; } diff --git a/test/Http/TestAsset/DummyRouteWithParam.php b/test/Http/TestAsset/DummyRouteWithParam.php index f694e685..87c85a41 100644 --- a/test/Http/TestAsset/DummyRouteWithParam.php +++ b/test/Http/TestAsset/DummyRouteWithParam.php @@ -5,7 +5,9 @@ namespace LaminasTest\Router\Http\TestAsset; use Laminas\Router\Http\RouteMatch; -use Laminas\Stdlib\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; + +use function strlen; /** * Dummy route. @@ -16,13 +18,16 @@ final class DummyRouteWithParam extends DummyRoute * match(): defined by RouteInterface interface. * * @see Route::match() - * - * @param int $pathOffset - * @return RouteMatch */ - public function match(RequestInterface $request, $pathOffset = null) - { - return new RouteMatch(['foo' => 'bar'], -4); + public function match( + ServerRequestInterface $request, + ?int $pathOffset = null, + array $options = [] + ): RouteMatch { + $pathLength = strlen($request->getUri()->getPath()); + $matchLength = $pathLength - ($pathOffset ?? 0); + + return new RouteMatch(['foo' => 'bar'], $matchLength); } /** @@ -32,7 +37,7 @@ public function match(RequestInterface $request, $pathOffset = null) * * @return mixed */ - public function assemble(?array $params = null, ?array $options = null) + public function assemble(?array $params = null, ?array $options = null): string { return $params['foo'] ?? ''; } diff --git a/test/Http/TranslatorAwareTreeRouteStackTest.php b/test/Http/TranslatorAwareTreeRouteStackTest.php index fb03f697..e3522005 100644 --- a/test/Http/TranslatorAwareTreeRouteStackTest.php +++ b/test/Http/TranslatorAwareTreeRouteStackTest.php @@ -4,27 +4,33 @@ namespace LaminasTest\Router\Http; -use Laminas\Http\Request; use Laminas\I18n\Translator\Translator; use Laminas\I18n\Translator\TranslatorAwareInterface; use Laminas\Router\Http\HttpRouteInterface; use Laminas\Router\Http\TranslatorAwareTreeRouteStack; +use Laminas\Translator\TranslatorInterface; use Laminas\Uri\Http as HttpUri; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; + +use function class_exists; final class TranslatorAwareTreeRouteStackTest extends TestCase { - /** @var string */ - protected $testFilesDir; + protected string $testFilesDir; - /** @var Translator */ - protected $translator; + protected TranslatorInterface $translator; - /** @var array */ - protected $fooRoute; + protected array $fooRoute; public function setUp(): void { + if (! class_exists(TranslatorAwareInterface::class)) { + $this->markTestSkipped('laminas-i18n is not installed'); + } + $this->testFilesDir = __DIR__ . '/_files'; $this->translator = new Translator(); @@ -84,10 +90,13 @@ public function testTranslatorAwareInterfaceImplementation(): void $this->assertFalse($stack->isTranslatorEnabled()); } + /** + * @throws ContainerExceptionInterface + */ public function testTranslatorIsPassedThroughMatchMethod(): void { $translator = new Translator(); - $request = new Request(); + $request = new MockServerRequest(new MockUri('http://example.com/')); $route = $this->createMock(HttpRouteInterface::class); $route->expects($this->once()) @@ -104,6 +113,9 @@ public function testTranslatorIsPassedThroughMatchMethod(): void $stack->match($request, null, ['translator' => $translator]); } + /** + * @throws ContainerExceptionInterface + */ public function testTranslatorIsPassedThroughAssembleMethod(): void { $translator = new Translator(); @@ -123,6 +135,9 @@ public function testTranslatorIsPassedThroughAssembleMethod(): void $stack->assemble([], ['name' => 'test', 'translator' => $translator, 'uri' => $uri]); } + /** + * @throws ContainerExceptionInterface + */ public function testAssembleRouteWithParameterLocale(): void { $stack = new TranslatorAwareTreeRouteStack(); @@ -136,6 +151,9 @@ public function testAssembleRouteWithParameterLocale(): void $this->assertEquals('/en/homepage', $stack->assemble(['locale' => 'en'], ['name' => 'foo/index'])); } + /** + * @throws ContainerExceptionInterface + */ public function testMatchRouteWithParameterLocale(): void { $stack = new TranslatorAwareTreeRouteStack(); @@ -145,8 +163,7 @@ public function testMatchRouteWithParameterLocale(): void $this->fooRoute ); - $request = new Request(); - $request->setUri('http://example.com/de/hauptseite'); + $request = new MockServerRequest(new MockUri('https://example.com/de/hauptseite')); $match = $stack->match($request); $this->assertNotNull($match); diff --git a/test/Http/TreeRouteStackTest.php b/test/Http/TreeRouteStackTest.php index b180bec1..4d1ba378 100644 --- a/test/Http/TreeRouteStackTest.php +++ b/test/Http/TreeRouteStackTest.php @@ -5,103 +5,105 @@ namespace LaminasTest\Router\Http; use ArrayIterator; -use Laminas\Http\PhpEnvironment\Request as PhpRequest; -use Laminas\Http\Request; use Laminas\Router\Exception\InvalidArgumentException; use Laminas\Router\Exception\RuntimeException; use Laminas\Router\Http\Hostname; use Laminas\Router\Http\TreeRouteStack; -use Laminas\Stdlib\Request as BaseRequest; +use Laminas\Router\RoutePluginManager; +use Laminas\ServiceManager\ServiceManager; use Laminas\Uri\Http as HttpUri; use LaminasTest\Router\FactoryTester; use LaminasTest\Router\TestAsset\DummyRoute; +use LaminasTest\Router\TestAsset\MockServerRequest; +use LaminasTest\Router\TestAsset\MockUri; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; use ReflectionClass; +use TypeError; final class TreeRouteStackTest extends TestCase { - public function testAddRouteRequiresHttpSpecificRoute() + private function createRoutePluginManager(): RoutePluginManager { - $stack = new TreeRouteStack(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Route definition must be an array or Traversable object'); - /** @psalm-suppress InvalidArgument we're explicitly testing runtime type validation here */ - $stack->addRoute('foo', new DummyRoute()); + return new RoutePluginManager(new ServiceManager(), [ + 'invokables' => [ + TestAsset\DummyRoute::class => TestAsset\DummyRoute::class, + TestAsset\DummyRouteWithParam::class => TestAsset\DummyRouteWithParam::class, + ], + ]); } - public function testAddRouteViaStringRequiresHttpSpecificRoute() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRouteViaStringRequiresHttpSpecificRoute(): void { - $stack = new TreeRouteStack(); + $plugins = new RoutePluginManager(new ServiceManager(), [ + 'invokables' => [ + DummyRoute::class => DummyRoute::class, + ], + ]); + $stack = new TreeRouteStack($plugins); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Given route does not implement HTTP route interface'); + $this->expectException(TypeError::class); $stack->addRoute('foo', [ 'type' => DummyRoute::class, ]); } - public function testAddRouteAcceptsTraversable() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRouteAcceptsTraversable(): void { - $stack = new TreeRouteStack(); + $stack = new TreeRouteStack($this->createRoutePluginManager()); $stack->addRoute('foo', new ArrayIterator([ 'type' => TestAsset\DummyRoute::class, ])); $this->assertTrue($stack->hasRoute('foo')); } - public function testNoMatchWithoutUriMethod() - { - $stack = new TreeRouteStack(); - $request = new BaseRequest(); - - $this->assertNull($stack->match($request)); - } - - public function testSetBaseUrlFromFirstMatch() + /** + * @throws ContainerExceptionInterface + */ + public function testBaseUrlLengthIsPassedAsOffset(): void { - $stack = new TreeRouteStack(); - - $request = new PhpRequest(); - $request->setBaseUrl('/foo'); - $stack->match($request); - $this->assertEquals('/foo', $stack->getBaseUrl()); - - $request = new PhpRequest(); - $request->setBaseUrl('/bar'); - $stack->match($request); - $this->assertEquals('/foo', $stack->getBaseUrl()); - } - - public function testBaseUrlLengthIsPassedAsOffset() - { - $stack = new TreeRouteStack(); + $stack = new TreeRouteStack($this->createRoutePluginManager()); $stack->setBaseUrl('/foo'); $stack->addRoute('foo', [ 'type' => TestAsset\DummyRoute::class, ]); - $this->assertEquals(4, $stack->match(new Request())->getParam('offset')); + $this->assertEquals(4, $stack->match(new MockServerRequest(new MockUri()))->getParam('offset')); } - public function testNoOffsetIsPassedWithoutBaseUrl() + /** + * @throws ContainerExceptionInterface + */ + public function testNoOffsetIsPassedWithoutBaseUrl(): void { - $stack = new TreeRouteStack(); + $stack = new TreeRouteStack($this->createRoutePluginManager()); $stack->addRoute('foo', [ 'type' => TestAsset\DummyRoute::class, ]); - $this->assertEquals(null, $stack->match(new Request())->getParam('offset')); + $this->assertEquals(null, $stack->match(new MockServerRequest(new MockUri()))->getParam('offset')); } - public function testAssemble() + /** + * @throws ContainerExceptionInterface + */ + public function testAssemble(): void { $stack = new TreeRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRoute()); $this->assertEquals('', $stack->assemble([], ['name' => 'foo'])); } - public function testAssembleCanonicalUriWithoutRequestUri() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleCanonicalUriWithoutRequestUri(): void { $stack = new TreeRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRoute()); @@ -111,42 +113,54 @@ public function testAssembleCanonicalUriWithoutRequestUri() $stack->assemble([], ['name' => 'foo', 'force_canonical' => true]); } - public function testAssembleCanonicalUriWithRequestUri() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleCanonicalUriWithRequestUri(): void { - $uri = new HttpUri('http://example.com:8080/'); + $uri = new HttpUri('https://example.com:8080/'); $stack = new TreeRouteStack(); $stack->setRequestUri($uri); $stack->addRoute('foo', new TestAsset\DummyRoute()); $this->assertEquals( - 'http://example.com:8080/', + 'https://example.com:8080/', $stack->assemble([], ['name' => 'foo', 'force_canonical' => true]) ); } - public function testAssembleCanonicalUriWithGivenUri() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleCanonicalUriWithGivenUri(): void { - $uri = new HttpUri('http://example.com:8080/'); + $uri = new HttpUri('https://example.com:8080/'); $stack = new TreeRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRoute()); $this->assertEquals( - 'http://example.com:8080/', + 'https://example.com:8080/', $stack->assemble([], ['name' => 'foo', 'uri' => $uri, 'force_canonical' => true]) ); } - public function testAssembleCanonicalUriWithHostnameRoute() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleCanonicalUriWithHostnameRoute(): void { $stack = new TreeRouteStack(); $stack->addRoute('foo', new Hostname('example.com')); $uri = new HttpUri(); - $uri->setScheme('http'); + $uri->setScheme('https'); - $this->assertEquals('http://example.com/', $stack->assemble([], ['name' => 'foo', 'uri' => $uri])); + $this->assertEquals('https://example.com/', $stack->assemble([], ['name' => 'foo', 'uri' => $uri])); } - public function testAssembleCanonicalUriWithHostnameRouteWithoutScheme() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleCanonicalUriWithHostnameRouteWithoutScheme(): void { $stack = new TreeRouteStack(); $stack->addRoute('foo', new Hostname('example.com')); @@ -157,18 +171,24 @@ public function testAssembleCanonicalUriWithHostnameRouteWithoutScheme() $stack->assemble([], ['name' => 'foo', 'uri' => $uri]); } - public function testAssembleCanonicalUriWithHostnameRouteAndRequestUriWithoutScheme() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleCanonicalUriWithHostnameRouteAndRequestUriWithoutScheme(): void { $uri = new HttpUri(); - $uri->setScheme('http'); + $uri->setScheme('https'); $stack = new TreeRouteStack(); $stack->setRequestUri($uri); $stack->addRoute('foo', new Hostname('example.com')); - $this->assertEquals('http://example.com/', $stack->assemble([], ['name' => 'foo'])); + $this->assertEquals('https://example.com/', $stack->assemble([], ['name' => 'foo'])); } - public function testAssembleWithQueryParams() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleWithQueryParams(): void { $stack = new TreeRouteStack(); $stack->addRoute( @@ -184,7 +204,10 @@ public function testAssembleWithQueryParams() $this->assertEquals('/?foo=bar', $stack->assemble([], ['name' => 'index', 'query' => ['foo' => 'bar']])); } - public function testAssembleWithEncodedPath() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleWithEncodedPath(): void { $stack = new TreeRouteStack(); $stack->addRoute( @@ -200,7 +223,10 @@ public function testAssembleWithEncodedPath() $this->assertEquals('/this%2Fthat', $stack->assemble([], ['name' => 'index'])); } - public function testAssembleWithEncodedPathAndQueryParams() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleWithEncodedPathAndQueryParams(): void { $stack = new TreeRouteStack(); $stack->addRoute( @@ -219,7 +245,10 @@ public function testAssembleWithEncodedPathAndQueryParams() ); } - public function testAssembleWithScheme() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleWithScheme(): void { $uri = new HttpUri(); $uri->setScheme('http'); @@ -246,7 +275,10 @@ public function testAssembleWithScheme() $this->assertEquals('https://example.com/', $stack->assemble([], ['name' => 'secure/index'])); } - public function testAssembleWithFragment() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleWithFragment(): void { $stack = new TreeRouteStack(); $stack->addRoute( @@ -262,7 +294,7 @@ public function testAssembleWithFragment() $this->assertEquals('/#foobar', $stack->assemble([], ['name' => 'index', 'fragment' => 'foobar'])); } - public function testAssembleWithoutNameOption() + public function testAssembleWithoutNameOption(): void { $stack = new TreeRouteStack(); @@ -271,7 +303,7 @@ public function testAssembleWithoutNameOption() $stack->assemble(); } - public function testAssembleNonExistentRoute() + public function testAssembleNonExistentRoute(): void { $stack = new TreeRouteStack(); @@ -280,7 +312,10 @@ public function testAssembleNonExistentRoute() $stack->assemble([], ['name' => 'foo']); } - public function testAssembleNonExistentChildRoute() + /** + * @throws ContainerExceptionInterface + */ + public function testAssembleNonExistentChildRoute(): void { $stack = new TreeRouteStack(); $stack->addRoute( @@ -298,27 +333,36 @@ public function testAssembleNonExistentChildRoute() $stack->assemble([], ['name' => 'index/foo']); } - public function testDefaultParamIsAddedToMatch() + /** + * @throws ContainerExceptionInterface + */ + public function testDefaultParamIsAddedToMatch(): void { $stack = new TreeRouteStack(); $stack->setBaseUrl('/foo'); $stack->addRoute('foo', new TestAsset\DummyRoute()); $stack->setDefaultParam('foo', 'bar'); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $this->assertEquals('bar', $stack->match(new MockServerRequest(new MockUri()))->getParam('foo')); } - public function testDefaultParamDoesNotOverrideParam() + /** + * @throws ContainerExceptionInterface + */ + public function testDefaultParamDoesNotOverrideParam(): void { $stack = new TreeRouteStack(); $stack->setBaseUrl('/foo'); $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); $stack->setDefaultParam('foo', 'baz'); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $this->assertEquals('bar', $stack->match(new MockServerRequest(new MockUri()))->getParam('foo')); } - public function testDefaultParamIsUsedForAssembling() + /** + * @throws ContainerExceptionInterface + */ + public function testDefaultParamIsUsedForAssembling(): void { $stack = new TreeRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); @@ -327,7 +371,10 @@ public function testDefaultParamIsUsedForAssembling() $this->assertEquals('bar', $stack->assemble([], ['name' => 'foo'])); } - public function testDefaultParamDoesNotOverrideParamForAssembling() + /** + * @throws ContainerExceptionInterface + */ + public function testDefaultParamDoesNotOverrideParamForAssembling(): void { $stack = new TreeRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); @@ -336,7 +383,7 @@ public function testDefaultParamDoesNotOverrideParamForAssembling() $this->assertEquals('bar', $stack->assemble(['foo' => 'bar'], ['name' => 'foo'])); } - public function testSetBaseUrl() + public function testSetBaseUrl(): void { $stack = new TreeRouteStack(); @@ -344,7 +391,7 @@ public function testSetBaseUrl() $this->assertEquals('/foo', $stack->getBaseUrl()); } - public function testSetRequestUri() + public function testSetRequestUri(): void { $uri = new HttpUri(); $stack = new TreeRouteStack(); @@ -353,7 +400,10 @@ public function testSetRequestUri() $this->assertEquals($uri, $stack->getRequestUri()); } - public function testPriorityIsPassedToPartRoute() + /** + * @throws ContainerExceptionInterface + */ + public function testPriorityIsPassedToPartRoute(): void { $stack = new TreeRouteStack(); $stack->addRoutes([ @@ -384,13 +434,17 @@ public function testPriorityIsPassedToPartRoute() $reflectedClass = new ReflectionClass($stack); $reflectedProperty = $reflectedClass->getProperty('routes'); - $reflectedProperty->setAccessible(true); - $routes = $reflectedProperty->getValue($stack); + $routes = $reflectedProperty->getValue($stack); + $fooRoute = $routes->get('foo'); - $this->assertEquals(1000, $routes->get('foo')->priority); + $this->assertInstanceOf(TreeRouteStack::class, $fooRoute); + $this->assertEquals(1000, $fooRoute->getPriority()); } - public function testPrototypeRoute() + /** + * @throws ContainerExceptionInterface + */ + public function testPrototypeRoute(): void { $stack = new TreeRouteStack(); $stack->addPrototype( @@ -401,7 +455,10 @@ public function testPrototypeRoute() $this->assertEquals('/bar', $stack->assemble([], ['name' => 'foo'])); } - public function testChainRouteAssembling() + /** + * @throws ContainerExceptionInterface + */ + public function testChainRouteAssembling(): void { $stack = new TreeRouteStack(); $stack->addPrototype( @@ -423,7 +480,10 @@ public function testChainRouteAssembling() $this->assertEquals('/foo/bar', $stack->assemble([], ['name' => 'foo'])); } - public function testChainRouteAssemblingWithChildrenAndSecureScheme() + /** + * @throws ContainerExceptionInterface + */ + public function testChainRouteAssemblingWithChildrenAndSecureScheme(): void { $stack = new TreeRouteStack(); @@ -454,7 +514,7 @@ public function testChainRouteAssemblingWithChildrenAndSecureScheme() $this->assertEquals('https://localhost/foo/baz', $stack->assemble([], ['name' => 'foo/baz'])); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( diff --git a/test/Http/WildcardTest.php b/test/Http/WildcardTest.php deleted file mode 100644 index 4175dd37..00000000 --- a/test/Http/WildcardTest.php +++ /dev/null @@ -1,222 +0,0 @@ - - * }> - */ - public static function routeProvider(): array - { - return [ - 'simple-match' => [ - new Wildcard(), - '/foo/bar/baz/bat', - null, - ['foo' => 'bar', 'baz' => 'bat'], - ], - 'empty-match' => [ - new Wildcard(), - '', - null, - [], - ], - 'no-match-without-leading-delimiter' => [ - new Wildcard(), - '/foo/foo/bar/baz/bat', - 5, - null, - ], - 'no-match-with-trailing-slash' => [ - new Wildcard(), - '/foo/bar/baz/bat/', - null, - null, - ], - 'match-overrides-default' => [ - new Wildcard('/', '/', ['foo' => 'baz']), - '/foo/bat', - null, - ['foo' => 'bat'], - ], - 'offset-skips-beginning' => [ - new Wildcard(), - '/bat/foo/bar', - 4, - ['foo' => 'bar'], - ], - 'non-standard-key-value-delimiter' => [ - new Wildcard('-'), - '/foo-bar/baz-bat', - null, - ['foo' => 'bar', 'baz' => 'bat'], - ], - 'non-standard-parameter-delimiter' => [ - new Wildcard('/', '-'), - '/foo/-foo/bar-baz/bat', - 5, - ['foo' => 'bar', 'baz' => 'bat'], - ], - 'empty-values-with-non-standard-key-value-delimiter-are-omitted' => [ - new Wildcard('-'), - '/foo', - null, - [], - true, - ], - 'url-encoded-parameters-are-decoded' => [ - new Wildcard(), - '/foo/foo%20bar', - null, - ['foo' => 'foo bar'], - ], - 'params-contain-non-string-scalar-values' => [ - new Wildcard(), - '/int_param/42/float_param/4.2', - null, - ['int_param' => 42, 'float_param' => 4.2], - ], - ]; - } - - /** - * @param string $path - * @param int|null $offset - */ - #[DataProvider('routeProvider')] - public function testMatching(Wildcard $route, $path, $offset, ?array $params = null) - { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset); - - if ($params === null) { - $this->assertNull($match); - } else { - $this->assertInstanceOf(RouteMatch::class, $match); - - if ($offset === null) { - $this->assertEquals(strlen($path), $match->getLength()); - } - - foreach ($params as $key => $value) { - $this->assertEquals($value, $match->getParam($key)); - } - } - } - - /** - * @param string $path - * @param int|null $offset - * @param boolean $skipAssembling - */ - #[DataProvider('routeProvider')] - public function testAssembling(Wildcard $route, $path, $offset, ?array $params = null, $skipAssembling = false) - { - if ($params === null || $skipAssembling) { - // Data which will not match are not tested for assembling. - $this->expectNotToPerformAssertions(); - return; - } - - $result = $route->assemble($params); - - if ($offset !== null) { - $this->assertEquals($offset, strpos($path, (string) $result, $offset)); - } else { - $this->assertEquals($path, $result); - } - } - - public function testNoMatchWithoutUriMethod() - { - $route = new Wildcard(); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); - } - - public function testGetAssembledParams() - { - $route = new Wildcard(); - $route->assemble(['foo' => 'bar']); - - $this->assertEquals(['foo'], $route->getAssembledParams()); - } - - public function testFactory() - { - $tester = new FactoryTester($this); - $tester->testFactory( - Wildcard::class, - [], - [] - ); - } - - public function testRawDecode() - { - // verify all characters which don't absolutely require encoding pass through match unchanged - // this includes every character other than #, %, / and ? - $raw = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`-=[]\\;\',.~!@$^&*()_+{}|:"<>'; - $request = new Request(); - $request->setUri('http://example.com/foo/' . $raw); - $route = new Wildcard(); - $match = $route->match($request); - - $this->assertSame($raw, $match->getParam('foo')); - } - - public function testEncodedDecode() - { - // @phpcs:disable Generic.Files.LineLength.TooLong - // every character - $in = '%61%62%63%64%65%66%67%68%69%6a%6b%6c%6d%6e%6f%70%71%72%73%74%75%76%77%78%79%7a%41%42%43%44%45%46%47%48%49%4a%4b%4c%4d%4e%4f%50%51%52%53%54%55%56%57%58%59%5a%30%31%32%33%34%35%36%37%38%39%60%2d%3d%5b%5d%5c%3b%27%2c%2e%2f%7e%21%40%23%24%25%5e%26%2a%28%29%5f%2b%7b%7d%7c%3a%22%3c%3e%3f'; - $out = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`-=[]\\;\',./~!@#$%^&*()_+{}|:"<>?'; - // @phpcs:enable Generic.Files.LineLength.TooLong - - $request = new Request(); - $request->setUri('http://example.com/foo/' . $in); - $route = new Wildcard(); - $match = $route->match($request); - - $this->assertSame($out, $match->getParam('foo')); - } - - public function testPathAssemblyShouldSkipAnyNonScalarValues() - { - /** @psalm-suppress DeprecatedClass */ - $route = new Wildcard('/', '/', [ - 'action' => 'index', - 'controller' => 'index', - 'middleware' => [ - ConnectMiddleware::class, - Handler::class, - ], - ]); - - $path = $route->assemble(); - $this->assertEquals('/action/index/controller/index', $path); - } -} diff --git a/test/PriorityListTest.php b/test/PriorityListTest.php deleted file mode 100644 index d21b28d7..00000000 --- a/test/PriorityListTest.php +++ /dev/null @@ -1,118 +0,0 @@ - */ - private PriorityList $list; - - public function setUp(): void - { - $this->list = new PriorityList(); - } - - public function testInsert(): void - { - $this->list->insert('foo', new TestAsset\DummyRoute(), 0); - - $this->assertCount(1, $this->list); - - $list = iterator_to_array($this->list); - $this->assertSame(['foo'], array_keys($list)); - } - - public function testRemove(): void - { - $this->list->insert('foo', new TestAsset\DummyRoute(), 0); - $this->list->insert('bar', new TestAsset\DummyRoute(), 0); - - $this->assertCount(2, $this->list); - - $this->list->remove('foo'); - - $this->assertCount(1, $this->list); - } - - public function testRemovingNonExistentRouteDoesNotYieldError(): void - { - $this->expectNotToPerformAssertions(); - $this->list->remove('foo'); - } - - public function testClear(): void - { - $this->list->insert('foo', new TestAsset\DummyRoute(), 0); - $this->list->insert('bar', new TestAsset\DummyRoute(), 0); - - $this->assertCount(2, $this->list); - - $this->list->clear(); - - $this->assertCount(0, $this->list); - $this->assertFalse($this->list->current()); - } - - public function testGet(): void - { - $route = new TestAsset\DummyRoute(); - - $this->list->insert('foo', $route, 0); - - $this->assertEquals($route, $this->list->get('foo')); - $this->assertNull($this->list->get('bar')); - } - - public function testLIFOOnly(): void - { - $this->list->insert('foo', new TestAsset\DummyRoute(), 0); - $this->list->insert('bar', new TestAsset\DummyRoute(), 0); - $this->list->insert('baz', new TestAsset\DummyRoute(), 0); - - $list = iterator_to_array($this->list); - - $this->assertEquals(['baz', 'bar', 'foo'], array_keys($list)); - } - - public function testPriorityOnly(): void - { - $this->list->insert('foo', new TestAsset\DummyRoute(), 1); - $this->list->insert('bar', new TestAsset\DummyRoute(), 0); - $this->list->insert('baz', new TestAsset\DummyRoute(), 2); - - $list = iterator_to_array($this->list); - - $this->assertEquals(['baz', 'foo', 'bar'], array_keys($list)); - } - - public function testLIFOWithPriority(): void - { - $this->list->insert('foo', new TestAsset\DummyRoute(), 0); - $this->list->insert('bar', new TestAsset\DummyRoute(), 0); - $this->list->insert('baz', new TestAsset\DummyRoute(), 1); - - $list = iterator_to_array($this->list); - - $this->assertEquals(['baz', 'bar', 'foo'], array_keys($list)); - } - - public function testPriorityWithNegativesAndNull(): void - { - $this->list->insert('foo', new TestAsset\DummyRoute(), null); - $this->list->insert('bar', new TestAsset\DummyRoute(), 1); - $this->list->insert('baz', new TestAsset\DummyRoute(), -1); - - $list = iterator_to_array($this->list); - - $this->assertEquals(['bar', 'foo', 'baz'], array_keys($list)); - } -} diff --git a/test/RouteContainerTest.php b/test/RouteContainerTest.php new file mode 100644 index 00000000..7c2af108 --- /dev/null +++ b/test/RouteContainerTest.php @@ -0,0 +1,116 @@ +container = new SimpleRouteContainer(); + } + + public function testInsert(): void + { + $this->container->insert('foo', new TestAsset\DummyRoute()); + + $this->assertCount(1, $this->container); + + $list = iterator_to_array($this->container); + $this->assertSame(['foo'], array_keys($list)); + } + + public function testRemove(): void + { + $this->container->insert('foo', new TestAsset\DummyRoute()); + $this->container->insert('bar', new TestAsset\DummyRoute()); + + $this->assertCount(2, $this->container); + + $this->container->remove('foo'); + + $this->assertCount(1, $this->container); + } + + public function testRemovingNonExistentRouteDoesNotYieldError(): void + { + $this->expectNotToPerformAssertions(); + $this->container->remove('foo'); + } + + public function testClear(): void + { + $this->container->insert('foo', new TestAsset\DummyRoute()); + $this->container->insert('bar', new TestAsset\DummyRoute()); + + $this->assertCount(2, $this->container); + + $this->container->clear(); + + $this->assertCount(0, $this->container); + $this->assertFalse($this->container->valid()); + } + + public function testGet(): void + { + $route = new TestAsset\DummyRoute(); + + $this->container->insert('foo', $route); + + $this->assertEquals($route, $this->container->get('foo')); + $this->assertNull($this->container->get('bar')); + } + + public function testLIFOOnly(): void + { + $this->container->insert('foo', new TestAsset\DummyRoute()); + $this->container->insert('bar', new TestAsset\DummyRoute()); + $this->container->insert('baz', new TestAsset\DummyRoute()); + + $list = iterator_to_array($this->container); + + $this->assertEquals(['baz', 'bar', 'foo'], array_keys($list)); + } + + public function testPriorityOnly(): void + { + $this->container->insert('foo', new TestAsset\DummyRoute(), 1); + $this->container->insert('bar', new TestAsset\DummyRoute()); + $this->container->insert('baz', new TestAsset\DummyRoute(), 2); + + $list = iterator_to_array($this->container); + + $this->assertEquals(['baz', 'foo', 'bar'], array_keys($list)); + } + + public function testLIFOWithPriority(): void + { + $this->container->insert('foo', new TestAsset\DummyRoute()); + $this->container->insert('bar', new TestAsset\DummyRoute()); + $this->container->insert('baz', new TestAsset\DummyRoute(), 1); + + $list = iterator_to_array($this->container); + + $this->assertEquals(['baz', 'bar', 'foo'], array_keys($list)); + } + + public function testPriorityWithNegativesAndNull(): void + { + $this->container->insert('foo', new TestAsset\DummyRoute(), null); + $this->container->insert('bar', new TestAsset\DummyRoute(), 1); + $this->container->insert('baz', new TestAsset\DummyRoute(), -1); + + $list = iterator_to_array($this->container); + + $this->assertEquals(['bar', 'foo', 'baz'], array_keys($list)); + } +} diff --git a/test/RoutePluginManagerFactoryTest.php b/test/RoutePluginManagerFactoryTest.php index dacf2fab..fcf5a600 100644 --- a/test/RoutePluginManagerFactoryTest.php +++ b/test/RoutePluginManagerFactoryTest.php @@ -13,8 +13,7 @@ final class RoutePluginManagerFactoryTest extends TestCase { - /** @var ContainerInterface|MockObject */ - private $container; + private MockObject&ContainerInterface $container; private RoutePluginManagerFactory $factory; public function setUp(): void diff --git a/test/RouterFactoryTest.php b/test/RouterFactoryTest.php index fda8923d..e850696f 100644 --- a/test/RouterFactoryTest.php +++ b/test/RouterFactoryTest.php @@ -7,25 +7,18 @@ use Laminas\Router\Http\HttpRouterFactory; use Laminas\Router\RoutePluginManager; use Laminas\Router\RouterFactory; -use Laminas\ServiceManager\Config; -use Laminas\ServiceManager\ConfigInterface; use Laminas\ServiceManager\ServiceManager; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use function array_merge_recursive; -/** - * @see ConfigInterface - * - * @psalm-import-type ServiceManagerConfigurationType from ConfigInterface - */ class RouterFactoryTest extends TestCase { - /** @psalm-var ServiceManagerConfigurationType */ - protected $defaultServiceConfig; + protected array $defaultServiceConfig; - /** @var HttpRouterFactory|RouterFactory */ - protected $factory; + protected RouterFactory|HttpRouterFactory $factory; public function setUp(): void { @@ -39,9 +32,13 @@ public function setUp(): void $this->factory = new RouterFactory(); } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ public function testFactoryCanCreateRouterBasedOnConfiguredName(): void { - $config = new Config(array_merge_recursive($this->defaultServiceConfig, [ + $config = array_merge_recursive($this->defaultServiceConfig, [ 'services' => [ 'config' => [ 'router' => [ @@ -49,17 +46,20 @@ public function testFactoryCanCreateRouterBasedOnConfiguredName(): void ], ], ], - ])); - $services = new ServiceManager(); - $config->configureServiceManager($services); + ]); + $services = new ServiceManager($config); $router = $this->factory->__invoke($services, 'router'); $this->assertInstanceOf(TestAsset\Router::class, $router); } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ public function testFactoryCanCreateRouterWhenOnlyHttpRouterConfigPresent(): void { - $config = new Config(array_merge_recursive($this->defaultServiceConfig, [ + $config = array_merge_recursive($this->defaultServiceConfig, [ 'services' => [ 'config' => [ 'router' => [ @@ -67,9 +67,8 @@ public function testFactoryCanCreateRouterWhenOnlyHttpRouterConfigPresent(): voi ], ], ], - ])); - $services = new ServiceManager(); - $config->configureServiceManager($services); + ]); + $services = new ServiceManager($config); $router = $this->factory->__invoke($services, 'router'); $this->assertInstanceOf(TestAsset\Router::class, $router); diff --git a/test/SimpleRouteStackTest.php b/test/SimpleRouteStackTest.php index a53677d9..91dc3258 100644 --- a/test/SimpleRouteStackTest.php +++ b/test/SimpleRouteStackTest.php @@ -19,13 +19,24 @@ use Laminas\Router\RoutePluginManager; use Laminas\Router\SimpleRouteStack; use Laminas\ServiceManager\ServiceManager; -use Laminas\Stdlib\Request; +use LaminasTest\Router\TestAsset\MockServerRequest; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; final class SimpleRouteStackTest extends TestCase { - public function testSetRoutePluginManager() + private function createRoutePluginManager(): RoutePluginManager + { + return new RoutePluginManager(new ServiceManager(), [ + 'invokables' => [ + TestAsset\DummyRoute::class => TestAsset\DummyRoute::class, + TestAsset\DummyRouteWithParam::class => TestAsset\DummyRouteWithParam::class, + ], + ]); + } + + public function testSetRoutePluginManager(): void { $routes = new RoutePluginManager(new ServiceManager()); $stack = new SimpleRouteStack(); @@ -34,73 +45,64 @@ public function testSetRoutePluginManager() $this->assertEquals($routes, $stack->getRoutePluginManager()); } - public function testAddRoutesWithInvalidArgument() - { - $stack = new SimpleRouteStack(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('addRoutes expects an array or Traversable set of routes'); - $stack->addRoutes('foo'); - } - - public function testAddRoutesAsArray() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRoutesAsArray(): void { $stack = new SimpleRouteStack(); $stack->addRoutes([ 'foo' => new TestAsset\DummyRoute(), ]); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertInstanceOf(RouteMatch::class, $stack->match(new MockServerRequest())); } - public function testAddRoutesAsTraversable() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRoutesAsTraversable(): void { $stack = new SimpleRouteStack(); $stack->addRoutes(new ArrayIterator([ 'foo' => new TestAsset\DummyRoute(), ])); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertInstanceOf(RouteMatch::class, $stack->match(new MockServerRequest())); } - public function testSetRoutesWithInvalidArgument() - { - $stack = new SimpleRouteStack(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('addRoutes expects an array or Traversable set of routes'); - $stack->setRoutes('foo'); - } - - public function testSetRoutesAsArray() + public function testSetRoutesAsArray(): void { $stack = new SimpleRouteStack(); $stack->setRoutes([ 'foo' => new TestAsset\DummyRoute(), ]); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertInstanceOf(RouteMatch::class, $stack->match(new MockServerRequest())); $stack->setRoutes([]); - $this->assertNull($stack->match(new Request())); + $this->assertNull($stack->match(new MockServerRequest())); } - public function testSetRoutesAsTraversable() + public function testSetRoutesAsTraversable(): void { $stack = new SimpleRouteStack(); $stack->setRoutes(new ArrayIterator([ 'foo' => new TestAsset\DummyRoute(), ])); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertInstanceOf(RouteMatch::class, $stack->match(new MockServerRequest())); $stack->setRoutes(new ArrayIterator([])); - $this->assertNull($stack->match(new Request())); + $this->assertNull($stack->match(new MockServerRequest())); } - public function testremoveRouteAsArray() + /** + * @throws ContainerExceptionInterface + */ + public function testremoveRouteAsArray(): void { $stack = new SimpleRouteStack(); $stack->addRoutes([ @@ -108,41 +110,40 @@ public function testremoveRouteAsArray() ]); $this->assertEquals($stack, $stack->removeRoute('foo')); - $this->assertNull($stack->match(new Request())); - } - - public function testAddRouteWithInvalidArgument() - { - $stack = new SimpleRouteStack(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Route definition must be an array or Traversable object'); - /** @psalm-suppress InvalidArgument we're explicitly verifying runtime type checks here */ - $stack->addRoute('foo', 'bar'); + $this->assertNull($stack->match(new MockServerRequest())); } - public function testAddRouteAsArrayWithoutOptions() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRouteAsArrayWithoutOptions(): void { - $stack = new SimpleRouteStack(); + $stack = new SimpleRouteStack($this->createRoutePluginManager()); $stack->addRoute('foo', [ 'type' => TestAsset\DummyRoute::class, ]); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertInstanceOf(RouteMatch::class, $stack->match(new MockServerRequest())); } - public function testAddRouteAsArrayWithOptions() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRouteAsArrayWithOptions(): void { - $stack = new SimpleRouteStack(); + $stack = new SimpleRouteStack($this->createRoutePluginManager()); $stack->addRoute('foo', [ 'type' => TestAsset\DummyRoute::class, 'options' => [], ]); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertInstanceOf(RouteMatch::class, $stack->match(new MockServerRequest())); } - public function testAddRouteAsArrayWithoutType() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRouteAsArrayWithoutType(): void { $stack = new SimpleRouteStack(); @@ -151,9 +152,12 @@ public function testAddRouteAsArrayWithoutType() $stack->addRoute('foo', []); } - public function testAddRouteAsArrayWithPriority() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRouteAsArrayWithPriority(): void { - $stack = new SimpleRouteStack(); + $stack = new SimpleRouteStack($this->createRoutePluginManager()); $stack->addRoute('foo', [ 'type' => TestAsset\DummyRouteWithParam::class, @@ -163,15 +167,18 @@ public function testAddRouteAsArrayWithPriority() 'priority' => 1, ]); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $this->assertEquals('bar', $stack->match(new MockServerRequest())->getParam('foo')); } - public function testAddRouteWithPriority() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRouteWithPriority(): void { - $stack = new SimpleRouteStack(); + $stack = new SimpleRouteStack($this->createRoutePluginManager()); - $route = new TestAsset\DummyRouteWithParam(); - $route->priority = 2; + $route = new TestAsset\DummyRouteWithParam(); + $route->setPriority(2); $stack->addRoute('baz', $route); $stack->addRoute('foo', [ @@ -179,27 +186,33 @@ public function testAddRouteWithPriority() 'priority' => 1, ]); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $this->assertEquals('bar', $stack->match(new MockServerRequest())->getParam('foo')); } - public function testAddRouteAsTraversable() + /** + * @throws ContainerExceptionInterface + */ + public function testAddRouteAsTraversable(): void { - $stack = new SimpleRouteStack(); + $stack = new SimpleRouteStack($this->createRoutePluginManager()); $stack->addRoute('foo', new ArrayIterator([ 'type' => TestAsset\DummyRoute::class, ])); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertInstanceOf(RouteMatch::class, $stack->match(new MockServerRequest())); } - public function testAssemble() + /** + * @throws ContainerExceptionInterface + */ + public function testAssemble(): void { $stack = new SimpleRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRoute()); $this->assertEquals('', $stack->assemble([], ['name' => 'foo'])); } - public function testAssembleWithoutNameOption() + public function testAssembleWithoutNameOption(): void { $stack = new SimpleRouteStack(); @@ -208,7 +221,7 @@ public function testAssembleWithoutNameOption() $stack->assemble(); } - public function testAssembleNonExistentRoute() + public function testAssembleNonExistentRoute(): void { $stack = new SimpleRouteStack(); @@ -217,25 +230,34 @@ public function testAssembleNonExistentRoute() $stack->assemble([], ['name' => 'foo']); } - public function testDefaultParamIsAddedToMatch() + /** + * @throws ContainerExceptionInterface + */ + public function testDefaultParamIsAddedToMatch(): void { $stack = new SimpleRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRoute()); $stack->setDefaultParam('foo', 'bar'); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $this->assertEquals('bar', $stack->match(new MockServerRequest())->getParam('foo')); } - public function testDefaultParamDoesNotOverrideParam() + /** + * @throws ContainerExceptionInterface + */ + public function testDefaultParamDoesNotOverrideParam(): void { $stack = new SimpleRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); $stack->setDefaultParam('foo', 'baz'); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $this->assertEquals('bar', $stack->match(new MockServerRequest())->getParam('foo')); } - public function testDefaultParamIsUsedForAssembling() + /** + * @throws ContainerExceptionInterface + */ + public function testDefaultParamIsUsedForAssembling(): void { $stack = new SimpleRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); @@ -244,7 +266,10 @@ public function testDefaultParamIsUsedForAssembling() $this->assertEquals('bar', $stack->assemble([], ['name' => 'foo'])); } - public function testDefaultParamDoesNotOverrideParamForAssembling() + /** + * @throws ContainerExceptionInterface + */ + public function testDefaultParamDoesNotOverrideParamForAssembling(): void { $stack = new SimpleRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); @@ -253,7 +278,7 @@ public function testDefaultParamDoesNotOverrideParamForAssembling() $this->assertEquals('bar', $stack->assemble(['foo' => 'bar'], ['name' => 'foo'])); } - public function testFactory() + public function testFactory(): void { $tester = new FactoryTester($this); $tester->testFactory( @@ -267,13 +292,16 @@ public function testFactory() ); } - public function testGetRoutes() + public function testGetRoutes(): void { $stack = new SimpleRouteStack(); $this->assertInstanceOf('Traversable', $stack->getRoutes()); } - public function testGetRouteByName() + /** + * @throws ContainerExceptionInterface + */ + public function testGetRouteByName(): void { $stack = new SimpleRouteStack(); $route = new TestAsset\DummyRoute(); @@ -282,13 +310,16 @@ public function testGetRouteByName() $this->assertEquals($route, $stack->getRoute('foo')); } - public function testHasRoute() + /** + * @throws ContainerExceptionInterface + */ + public function testHasRoute(): void { $stack = new SimpleRouteStack(); - $this->assertEquals(false, $stack->hasRoute('foo')); + $this->assertFalse($stack->hasRoute('foo')); $stack->addRoute('foo', new TestAsset\DummyRoute()); - $this->assertEquals(true, $stack->hasRoute('foo')); + $this->assertTrue($stack->hasRoute('foo')); } /** @return array */ @@ -388,14 +419,19 @@ public static function routeTypeProvider(): array ]; } + /** + * @throws ContainerExceptionInterface + */ #[DataProvider('routeTypeProvider')] - public function testSimpleRouteStackSetsPriorityForAllKnownRouteTypes(array $routeSpec, int $expectedPriority): void - { + public function testSimpleRouteStackSetsPriorityForAllKnownRouteTypes( + array $routeSpec, + int $expectedPriority + ): void { $router = new SimpleRouteStack(); $router->addRoute('name', $routeSpec); $route = $router->getRoute('name'); self::assertNotNull($route); - self::assertEquals($expectedPriority, $route->priority); + self::assertEquals($expectedPriority, $route->getPriority()); } } diff --git a/test/TestAsset/DummyRoute.php b/test/TestAsset/DummyRoute.php index 805f3ec4..3bf9abb6 100644 --- a/test/TestAsset/DummyRoute.php +++ b/test/TestAsset/DummyRoute.php @@ -6,24 +6,22 @@ use Laminas\Router\RouteInterface; use Laminas\Router\RouteMatch; -use Laminas\Stdlib\RequestInterface; +use Laminas\Router\RoutePriorityTrait; +use Psr\Http\Message\ServerRequestInterface; /** * Dummy route. */ class DummyRoute implements RouteInterface { - /** @deprecated Setting priority with a public property should be factored out in the next major */ - public ?int $priority = null; + use RoutePriorityTrait; /** * match(): defined by RouteInterface interface. * * @see Route::match() - * - * @return RouteMatch */ - public function match(RequestInterface $request) + public function match(ServerRequestInterface $request): ?RouteMatch { return new RouteMatch([]); } @@ -32,10 +30,8 @@ public function match(RequestInterface $request) * assemble(): defined by RouteInterface interface. * * @see Route::assemble() - * - * @return mixed */ - public function assemble(?array $params = null, ?array $options = null) + public function assemble(?array $params = null, ?array $options = null): string { return ''; } @@ -43,10 +39,9 @@ public function assemble(?array $params = null, ?array $options = null) /** * factory(): defined by RouteInterface interface * - * @param iterable $options * @return DummyRoute */ - public static function factory($options = []) + public static function factory(iterable $options = []): RouteInterface { return new static(); } diff --git a/test/TestAsset/DummyRouteWithParam.php b/test/TestAsset/DummyRouteWithParam.php index 32a21c7e..05793f6b 100644 --- a/test/TestAsset/DummyRouteWithParam.php +++ b/test/TestAsset/DummyRouteWithParam.php @@ -5,7 +5,7 @@ namespace LaminasTest\Router\TestAsset; use Laminas\Router\RouteMatch; -use Laminas\Stdlib\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; /** * Dummy route. @@ -16,10 +16,8 @@ final class DummyRouteWithParam extends DummyRoute * match(): defined by RouteInterface interface. * * @see Route::match() - * - * @return RouteMatch */ - public function match(RequestInterface $request) + public function match(ServerRequestInterface $request): ?RouteMatch { return new RouteMatch(['foo' => 'bar']); } @@ -31,7 +29,7 @@ public function match(RequestInterface $request) * * @return mixed */ - public function assemble(?array $params = null, ?array $options = null) + public function assemble(?array $params = null, ?array $options = null): string { return $params['foo'] ?? ''; } diff --git a/test/TestAsset/MockServerRequest.php b/test/TestAsset/MockServerRequest.php new file mode 100644 index 00000000..66d5d8c2 --- /dev/null +++ b/test/TestAsset/MockServerRequest.php @@ -0,0 +1,417 @@ + */ + private array $serverParams; + + /** @var array */ + private array $cookieParams = []; + + /** @var array */ + private array $queryParams = []; + + /** @var array> */ + private array $headers = []; + + /** @var array */ + private array $attributes = []; + + private string $protocolVersion = '1.1'; + + /** @var array */ + private array $uploadedFiles = []; + + private null|array|object $parsedBody = null; + + private StreamInterface $body; + + public function __construct( + ?UriInterface $uri = null, + string $method = 'GET', + array $serverParams = [], + ?StreamInterface $body = null + ) { + $this->uri = $uri ?? new MockUri(); + $this->method = $method; + $this->serverParams = $serverParams; + $this->body = $body ?? new MockStream(); + } + + /** + * Retrieves the HTTP protocol version as a string. + */ + public function getProtocolVersion(): string + { + return $this->protocolVersion; + } + + /** + * Return an instance with the specified HTTP protocol version. + * + * @param string $version HTTP protocol version + * @return static + */ + public function withProtocolVersion(string $version): ServerRequestInterface + { + $new = clone $this; + $new->protocolVersion = $version; + + return $new; + } + + /** + * Retrieves all message header values. + * + * @return string[][] Returns an associative array of the message's headers. + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + */ + public function hasHeader(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return string[] + */ + public function getHeader(string $name): array + { + return $this->headers[strtolower($name)] ?? []; + } + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * @param string $name Case-insensitive header field name. + */ + public function getHeaderLine(string $name): string + { + return implode(', ', $this->getHeader($name)); + } + + /** + * Return an instance with the provided value replacing the specified header. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @throws InvalidArgumentException For invalid header names or values. + * @return static + */ + public function withHeader(string $name, mixed $value): ServerRequestInterface + { + $new = clone $this; + $new->headers[strtolower($name)] = is_array($value) ? $value : [$value]; + + return $new; + } + + /** + * Return an instance with the specified header appended with the given value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @throws InvalidArgumentException For invalid header names or values. + * @return static + */ + public function withAddedHeader(string $name, mixed $value): ServerRequestInterface + { + $new = clone $this; + $existing = $new->headers[strtolower($name)] ?? []; + $new->headers[strtolower($name)] = array_merge($existing, is_array($value) ? $value : [$value]); + + return $new; + } + + /** + * Return an instance without the specified header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader(string $name): ServerRequestInterface + { + $new = clone $this; + unset($new->headers[strtolower($name)]); + + return $new; + } + + /** + * Gets the body of the message. + */ + public function getBody(): StreamInterface + { + return $this->body; + } + + /** + * Return an instance with the specified message body. + * + * @return static + */ + public function withBody(StreamInterface $body): ServerRequestInterface + { + $new = clone $this; + $new->body = $body; + + return $new; + } + + /** + * Retrieves the message's request target. + */ + public function getRequestTarget(): string + { + return $this->uri->getPath() ?: '/'; + } + + /** + * Return an instance with the specific request-target. + * + * @return static + */ + public function withRequestTarget(string $requestTarget): ServerRequestInterface + { + $uri = $this->getUri()->withPath($requestTarget); + + $new = clone $this; + $new->uri = $uri; + + return $new; + } + + /** + * Retrieves the HTTP method of the request. + */ + public function getMethod(): string + { + return $this->method; + } + + /** + * Return an instance with the provided HTTP method. + * + * @param string $method Case-sensitive method. + * @throws InvalidArgumentException For invalid HTTP methods. + * @return static + */ + public function withMethod(string $method): ServerRequestInterface + { + $new = clone $this; + $new->method = $method; + + return $new; + } + + /** + * Retrieves the URI instance. + */ + public function getUri(): UriInterface + { + return $this->uri; + } + + /** + * Returns an instance with the provided URI. + * + * @param UriInterface $uri New request URI to use. + * @param bool $preserveHost Preserve the original state of the Host header. + * @return static + */ + public function withUri(UriInterface $uri, bool $preserveHost = false): ServerRequestInterface + { + $new = clone $this; + $new->uri = $uri; + + return $new; + } + + /** + * Retrieve server parameters. + */ + public function getServerParams(): array + { + return $this->serverParams; + } + + /** + * Retrieve cookies. + */ + public function getCookieParams(): array + { + return $this->cookieParams; + } + + /** + * Return an instance with the specified cookies. + * + * @param array $cookies Array of key/value pairs representing cookies. + * @return static + */ + public function withCookieParams(array $cookies): ServerRequestInterface + { + $new = clone $this; + $new->cookieParams = $cookies; + + return $new; + } + + /** + * Retrieve query string arguments. + */ + public function getQueryParams(): array + { + return $this->queryParams; + } + + /** + * Return an instance with the specified query string arguments. + * + * @param array $query Array of query string arguments, typically from $_GET. + * @return static + */ + public function withQueryParams(array $query): ServerRequestInterface + { + $new = clone $this; + $new->queryParams = $query; + + return $new; + } + + /** + * Retrieve normalized file upload data. + * + * @return UploadedFileInterface[] An array tree of UploadedFileInterface instances. + */ + public function getUploadedFiles(): array + { + return $this->uploadedFiles; + } + + /** + * Create a new instance with the specified uploaded files. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + */ + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface + { + $new = clone $this; + $new->uploadedFiles = $uploadedFiles; + + return $new; + } + + /** + * Retrieve any parameters provided in the request body. + * + * @return null|array|object The deserialized body parameters, if any. + */ + public function getParsedBody(): object|array|null + { + return $this->parsedBody; + } + + /** + * Return an instance with the specified body parameters. + * + * @param null|array|object $data The deserialized body data. + * @throws InvalidArgumentException If an unsupported argument type is provided. + * @return static + */ + public function withParsedBody($data): ServerRequestInterface + { + $new = clone $this; + $new->parsedBody = $data; + + return $new; + } + + /** + * Retrieve attributes derived from the request. + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * Retrieve a single derived request attribute. + * + * @see getAttributes() + * + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + */ + public function getAttribute(string $name, mixed $default = null): mixed + { + return $this->attributes[$name] ?? $default; + } + + /** + * Return an instance with the specified derived request attribute. + * + * @see getAttributes() + * + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute(string $name, mixed $value): ServerRequestInterface + { + $new = clone $this; + $new->attributes[$name] = $value; + + return $new; + } + + /** + * Return an instance that removes the specified derived request attribute. + * + * @see getAttributes() + * + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute(string $name): ServerRequestInterface + { + $new = clone $this; + unset($new->attributes[$name]); + + return $new; + } +} diff --git a/test/TestAsset/MockStream.php b/test/TestAsset/MockStream.php new file mode 100644 index 00000000..247b5e63 --- /dev/null +++ b/test/TestAsset/MockStream.php @@ -0,0 +1,124 @@ +content; + } + + public function close(): void + { + $this->content = ''; + $this->readable = false; + $this->writable = false; + $this->seekable = false; + } + + public function detach(): void + { + $this->readable = false; + $this->writable = false; + $this->seekable = false; + } + + public function getSize(): ?int + { + return strlen($this->content); + } + + public function tell(): int + { + return 0; + } + + public function eof(): bool + { + return true; + } + + public function isSeekable(): bool + { + return $this->seekable; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + if (! $this->seekable) { + throw new RuntimeException('Stream is not seekable'); + } + } + + public function rewind(): void + { + $this->seek(0); + } + + public function isWritable(): bool + { + return $this->writable; + } + + public function write(string $string): int + { + if (! $this->writable) { + throw new RuntimeException('Stream is not writable'); + } + + $this->content .= $string; + + return strlen($string); + } + + public function isReadable(): bool + { + return $this->readable; + } + + public function read(int $length): string + { + if (! $this->readable) { + throw new RuntimeException('Stream is not readable'); + } + + return substr($this->content, 0, $length); + } + + public function getContents(): string + { + if (! $this->readable) { + throw new RuntimeException('Stream is not readable'); + } + + return $this->content; + } + + public function getMetadata(?string $key = null): mixed + { + return $key === null ? [] : null; + } +} diff --git a/test/TestAsset/MockUri.php b/test/TestAsset/MockUri.php new file mode 100644 index 00000000..17976d41 --- /dev/null +++ b/test/TestAsset/MockUri.php @@ -0,0 +1,146 @@ +uri = new HttpUri($uri); + } + + public function getScheme(): string + { + return $this->uri->getScheme() ?? ''; + } + + public function getAuthority(): string + { + $host = $this->getHost(); + if ($host === '') { + return ''; + } + + $authority = $host; + $userInfo = $this->getUserInfo(); + + if ($userInfo !== '') { + $authority = $userInfo . '@' . $authority; + } + + $port = $this->getPort(); + if ($port !== null) { + $authority .= ':' . $port; + } + + return $authority; + } + + public function getUserInfo(): string + { + return $this->uri->getUserInfo() ?? ''; + } + + public function getHost(): string + { + return $this->uri->getHost() ?? ''; + } + + public function getPort(): ?int + { + return $this->uri->getPort(); + } + + public function getPath(): string + { + $path = $this->uri->getPath() ?? ''; + + // For HTTP(S) URIs with a host but no path, normalize to '/' + if ($this->getHost() !== '' && $path === '') { + return '/'; + } + + return $path; + } + + public function getQuery(): string + { + return $this->uri->getQuery() ?? ''; + } + + public function getFragment(): string + { + return $this->uri->getFragment() ?? ''; + } + + public function withScheme(string $scheme): UriInterface + { + $new = clone $this; + $new->uri = clone $this->uri; + $new->uri->setScheme($scheme); + return $new; + } + + public function withUserInfo(string $user, ?string $password = null): UriInterface + { + $new = clone $this; + $new->uri = clone $this->uri; + $new->uri->setUserInfo($password !== null ? $user . ':' . $password : $user); + return $new; + } + + public function withHost(string $host): UriInterface + { + $new = clone $this; + $new->uri = clone $this->uri; + $new->uri->setHost($host); + return $new; + } + + public function withPort(?int $port): UriInterface + { + $new = clone $this; + $new->uri = clone $this->uri; + $new->uri->setPort($port); + return $new; + } + + public function withPath(string $path): UriInterface + { + $new = clone $this; + $new->uri = clone $this->uri; + $new->uri->setPath($path); + return $new; + } + + public function withQuery(string $query): UriInterface + { + $new = clone $this; + $new->uri = clone $this->uri; + $new->uri->setQuery($query); + return $new; + } + + public function withFragment(string $fragment): UriInterface + { + $new = clone $this; + $new->uri = clone $this->uri; + $new->uri->setFragment($fragment); + return $new; + } + + public function __toString(): string + { + return $this->uri->toString(); + } +} diff --git a/test/TestAsset/Router.php b/test/TestAsset/Router.php index b40f851d..fa3044bd 100644 --- a/test/TestAsset/Router.php +++ b/test/TestAsset/Router.php @@ -6,64 +6,59 @@ namespace LaminasTest\Router\TestAsset; use Laminas\Router\RouteInterface; +use Laminas\Router\RouteMatch; +use Laminas\Router\RoutePriorityTrait; use Laminas\Router\RouteStackInterface; -use Laminas\Stdlib\RequestInterface as Request; +use Psr\Http\Message\ServerRequestInterface; -/** - * @template TRoute of RouteInterface - * @template-implements RouteStackInterface - */ final class Router implements RouteStackInterface { + use RoutePriorityTrait; + /** * Create a new route with given options. * - * @param iterable $options * @return self */ - public static function factory($options = []) + public static function factory(iterable $options = []): RouteInterface { - return new static(); + return new Router(); } /** * Match a given request. - * - * @return RouteMatch|null */ - public function match(Request $request) + public function match(ServerRequestInterface $request): ?RouteMatch { } /** * Assemble the route. - * - * @return mixed */ - public function assemble(array $params = [], array $options = []) + public function assemble(array $params = [], array $options = []): mixed { } /** @inheritDoc */ - public function addRoute($name, $route, $priority = null) + public function addRoute($name, $route, $priority = null): RouteInterface { return $this; } /** @inheritDoc */ - public function addRoutes($routes) + public function addRoutes($routes): RouteStackInterface { return $this; } /** @inheritDoc */ - public function removeRoute($name) + public function removeRoute($name): RouteStackInterface { return $this; } /** @inheritDoc */ - public function setRoutes($routes) + public function setRoutes($routes): RouteStackInterface { return $this; } From 77ecbc8eca8e306e7067d3dcb8db82e2aed959d7 Mon Sep 17 00:00:00 2001 From: Simon Mundy <46739456+simon-mundy@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:43:15 +1100 Subject: [PATCH 2/4] Update .laminas-ci.json Signed-off-by: Simon Mundy <46739456+simon-mundy@users.noreply.github.com> --- .laminas-ci.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.laminas-ci.json b/.laminas-ci.json index 57d29d6b..be23f935 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -1,5 +1,6 @@ { "ignore_php_platform_requirements": { "8.5": true - } + }, + "backwardCompatibilityCheck": true } From 03ad98e5d794063c2376c701cba80dc95f6d5d45 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Sun, 22 Feb 2026 10:04:17 +1100 Subject: [PATCH 3/4] - Removed RouterFactory and updated aliases - Updated tests to reflect new factory change - Removed unnecessary ServiceListener check - Changed string literal to class check in RouterConfigTrait.php Signed-off-by: Simon Mundy --- psalm-baseline.xml | 24 ++------- src/ConfigProvider.php | 10 ++-- src/RoutePluginManagerFactory.php | 9 +--- src/RouterConfigTrait.php | 4 +- src/RouterFactory.php | 26 ---------- test/Http/HttpRouterFactoryTest.php | 54 +++++++++++++++++++- test/RouterFactoryTest.php | 76 ----------------------------- 7 files changed, 66 insertions(+), 137 deletions(-) delete mode 100644 src/RouterFactory.php delete mode 100644 test/RouterFactoryTest.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 75f06f6d..4cc8bedb 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -331,11 +331,6 @@ - - - get('HttpRouter')]]> - - @@ -396,6 +391,10 @@ + + + + @@ -593,21 +592,6 @@ - - - - - - new RoutePluginManager($services)]]> - - - - - - - - - diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 9b2c1034..f26f9a4a 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -46,15 +46,15 @@ public function getDependencyConfig() { return [ 'aliases' => [ - 'HttpRouter' => Http\TreeRouteStack::class, - 'router' => RouteStackInterface::class, - 'Router' => RouteStackInterface::class, - 'RoutePluginManager' => RoutePluginManager::class, + 'HttpRouter' => Http\TreeRouteStack::class, + 'router' => RouteStackInterface::class, + 'Router' => RouteStackInterface::class, + 'RoutePluginManager' => RoutePluginManager::class, + RouteStackInterface::class => Http\TreeRouteStack::class, ], 'factories' => [ Http\TreeRouteStack::class => Http\HttpRouterFactory::class, RoutePluginManager::class => RoutePluginManagerFactory::class, - RouteStackInterface::class => RouterFactory::class, ], ]; } diff --git a/src/RoutePluginManagerFactory.php b/src/RoutePluginManagerFactory.php index 95647222..a92e97bd 100644 --- a/src/RoutePluginManagerFactory.php +++ b/src/RoutePluginManagerFactory.php @@ -12,7 +12,8 @@ /** * @psalm-import-type ServiceManagerConfiguration from ServiceManager - */final class RoutePluginManagerFactory implements FactoryInterface + */ +final class RoutePluginManagerFactory implements FactoryInterface { /** * Create and return a route plugin manager. @@ -22,12 +23,6 @@ public function __invoke( string $requestedName, ?array $options = null ): RoutePluginManager { - // If this is in a laminas-mvc application, the ServiceListener will inject - // merged configuration during bootstrap. - if ($container->has('ServiceListener')) { - return new RoutePluginManager($container); - } - // If we do not have a config service, nothing more to do if (! $container->has('config')) { return new RoutePluginManager($container, $options ?? []); diff --git a/src/RouterConfigTrait.php b/src/RouterConfigTrait.php index cdad443e..1a760dcf 100644 --- a/src/RouterConfigTrait.php +++ b/src/RouterConfigTrait.php @@ -26,8 +26,8 @@ private function createRouter(string $class, array $config, ContainerInterface $ } // Inject the route plugins - if (! isset($config['route_plugins']) && $container->has('RoutePluginManager')) { - $routePluginManager = $container->get('RoutePluginManager'); + if (! isset($config['route_plugins']) && $container->has(RoutePluginManager::class)) { + $routePluginManager = $container->get(RoutePluginManager::class); if ($routePluginManager instanceof RoutePluginManager) { $config['route_plugins'] = $routePluginManager; } diff --git a/src/RouterFactory.php b/src/RouterFactory.php deleted file mode 100644 index 0cca8e42..00000000 --- a/src/RouterFactory.php +++ /dev/null @@ -1,26 +0,0 @@ -get('HttpRouter'); - } -} diff --git a/test/Http/HttpRouterFactoryTest.php b/test/Http/HttpRouterFactoryTest.php index 18a6e600..a53df7de 100644 --- a/test/Http/HttpRouterFactoryTest.php +++ b/test/Http/HttpRouterFactoryTest.php @@ -6,10 +6,20 @@ use Laminas\Router\Http\HttpRouterFactory; use Laminas\Router\RoutePluginManager; -use LaminasTest\Router\RouterFactoryTest as TestCase; +use Laminas\ServiceManager\ServiceManager; +use LaminasTest\Router\TestAsset; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +use function array_merge_recursive; final class HttpRouterFactoryTest extends TestCase { + private array $defaultServiceConfig; + + private HttpRouterFactory $factory; + public function setUp(): void { $this->defaultServiceConfig = [ @@ -20,4 +30,46 @@ public function setUp(): void $this->factory = new HttpRouterFactory(); } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testFactoryCanCreateRouterBasedOnConfiguredName(): void + { + $config = array_merge_recursive($this->defaultServiceConfig, [ + 'services' => [ + 'config' => [ + 'router' => [ + 'router_class' => TestAsset\Router::class, + ], + ], + ], + ]); + $services = new ServiceManager($config); + + $router = $this->factory->__invoke($services, 'router'); + $this->assertInstanceOf(TestAsset\Router::class, $router); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testFactoryCanCreateRouterWhenOnlyHttpRouterConfigPresent(): void + { + $config = array_merge_recursive($this->defaultServiceConfig, [ + 'services' => [ + 'config' => [ + 'router' => [ + 'router_class' => TestAsset\Router::class, + ], + ], + ], + ]); + $services = new ServiceManager($config); + + $router = $this->factory->__invoke($services, 'router'); + $this->assertInstanceOf(TestAsset\Router::class, $router); + } } diff --git a/test/RouterFactoryTest.php b/test/RouterFactoryTest.php deleted file mode 100644 index e850696f..00000000 --- a/test/RouterFactoryTest.php +++ /dev/null @@ -1,76 +0,0 @@ -defaultServiceConfig = [ - 'factories' => [ - 'HttpRouter' => HttpRouterFactory::class, - 'RoutePluginManager' => static fn($services) => new RoutePluginManager($services), - ], - ]; - - $this->factory = new RouterFactory(); - } - - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - public function testFactoryCanCreateRouterBasedOnConfiguredName(): void - { - $config = array_merge_recursive($this->defaultServiceConfig, [ - 'services' => [ - 'config' => [ - 'router' => [ - 'router_class' => TestAsset\Router::class, - ], - ], - ], - ]); - $services = new ServiceManager($config); - - $router = $this->factory->__invoke($services, 'router'); - $this->assertInstanceOf(TestAsset\Router::class, $router); - } - - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - public function testFactoryCanCreateRouterWhenOnlyHttpRouterConfigPresent(): void - { - $config = array_merge_recursive($this->defaultServiceConfig, [ - 'services' => [ - 'config' => [ - 'router' => [ - 'router_class' => TestAsset\Router::class, - ], - ], - ], - ]); - $services = new ServiceManager($config); - - $router = $this->factory->__invoke($services, 'router'); - $this->assertInstanceOf(TestAsset\Router::class, $router); - } -} From c08129416f7c8069c92fb41fc617f7a91525cad5 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Sun, 22 Feb 2026 10:17:42 +1100 Subject: [PATCH 4/4] - Added RoutePriorityTrait to DummyRoute.php - Minor fixes required after merge in github - Cleanup psalm baseline Signed-off-by: Simon Mundy --- psalm-baseline.xml | 139 ++++++---------------------------- src/Http/TreeRouteStack.php | 36 +-------- test/TestAsset/DummyRoute.php | 6 +- 3 files changed, 24 insertions(+), 157 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7fdccef3..4abc9675 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -145,17 +145,6 @@ - - - - - - - matchedRouteName === null]]> - - - - @@ -257,50 +246,9 @@ - - [ - 'chain' => Chain::class, - 'Chain' => Chain::class, - 'hostname' => Hostname::class, - 'Hostname' => Hostname::class, - 'hostName' => Hostname::class, - 'HostName' => Hostname::class, - 'literal' => Literal::class, - 'Literal' => Literal::class, - 'method' => Method::class, - 'Method' => Method::class, - 'part' => Part::class, - 'Part' => Part::class, - 'regex' => Regex::class, - 'Regex' => Regex::class, - 'scheme' => Scheme::class, - 'Scheme' => Scheme::class, - 'segment' => Segment::class, - 'Segment' => Segment::class, - ], - 'factories' => [ - Chain::class => RouteInvokableFactory::class, - Hostname::class => RouteInvokableFactory::class, - Literal::class => RouteInvokableFactory::class, - Method::class => RouteInvokableFactory::class, - Part::class => RouteInvokableFactory::class, - Regex::class => RouteInvokableFactory::class, - Scheme::class => RouteInvokableFactory::class, - Segment::class => RouteInvokableFactory::class, - ], - ])]]> - - - - baseUrl === null]]> - requestUri === null]]> - requestUri === null]]> - requestUri === null]]> - getHost() === null || $uri->getScheme() === null) && $this->requestUri === null]]> - - - + + + @@ -323,6 +271,9 @@ assemble(array_merge($this->defaultParams, $params), $options)]]> assemble(array_merge($this->defaultParams, $params), $options)]]> + + + @@ -333,33 +284,15 @@ - - - - - - - - - - - - - - - baseUrl]]> - - - - - - - + + + - - - - + + + + + @@ -432,11 +365,10 @@ - - - - - + + + + @@ -468,9 +400,9 @@ - - - + + + [ @@ -490,41 +422,17 @@ - - - - - - - - ]]> - - - - - - - - - - - - - - - - @@ -646,11 +554,8 @@ - - - - + diff --git a/src/Http/TreeRouteStack.php b/src/Http/TreeRouteStack.php index 6bec6092..a7e90c7c 100644 --- a/src/Http/TreeRouteStack.php +++ b/src/Http/TreeRouteStack.php @@ -110,43 +110,9 @@ public static function factory(iterable $options = []): RouteStackInterface * * @see SimpleRouteStack::init() */ - protected function init() + protected function init(): void { - /** @var ArrayObject $this->prototypes */ $this->prototypes = new ArrayObject(); - - (new Config([ - 'aliases' => [ - 'chain' => Chain::class, - 'Chain' => Chain::class, - 'hostname' => Hostname::class, - 'Hostname' => Hostname::class, - 'hostName' => Hostname::class, - 'HostName' => Hostname::class, - 'literal' => Literal::class, - 'Literal' => Literal::class, - 'method' => Method::class, - 'Method' => Method::class, - 'part' => Part::class, - 'Part' => Part::class, - 'regex' => Regex::class, - 'Regex' => Regex::class, - 'scheme' => Scheme::class, - 'Scheme' => Scheme::class, - 'segment' => Segment::class, - 'Segment' => Segment::class, - ], - 'factories' => [ - Chain::class => RouteInvokableFactory::class, - Hostname::class => RouteInvokableFactory::class, - Literal::class => RouteInvokableFactory::class, - Method::class => RouteInvokableFactory::class, - Part::class => RouteInvokableFactory::class, - Regex::class => RouteInvokableFactory::class, - Scheme::class => RouteInvokableFactory::class, - Segment::class => RouteInvokableFactory::class, - ], - ]))->configureServiceManager($this->routePluginManager); } /** diff --git a/test/TestAsset/DummyRoute.php b/test/TestAsset/DummyRoute.php index fdccd091..3bf9abb6 100644 --- a/test/TestAsset/DummyRoute.php +++ b/test/TestAsset/DummyRoute.php @@ -14,11 +14,7 @@ */ class DummyRoute implements RouteInterface { - /** - * @internal - * @deprecated Since 3.9.0 This property will be removed or made private in version 4.0 - */ - public ?int $priority = null; + use RoutePriorityTrait; /** * match(): defined by RouteInterface interface.