From cbc7fc8ba4748577de67cc032e0128d1e0f6348e Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 12 Oct 2017 17:40:03 +1000 Subject: [PATCH 01/47] Bump minimum php to 7.1 --- .travis.yml | 23 +--- README.md | 19 +++- composer.json | 9 +- composer.lock | 307 ++++++++++++++++++++++++++------------------------ 4 files changed, 182 insertions(+), 176 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2fb1fd4..1aeacca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,26 +13,6 @@ env: matrix: include: - - php: 5.6 - env: - - DEPS=lowest - - php: 5.6 - env: - - DEPS=locked - - LEGACY_DEPS="phpunit/phpunit" - - php: 5.6 - env: - - DEPS=latest - - php: 7 - env: - - DEPS=lowest - - php: 7 - env: - - DEPS=locked - - LEGACY_DEPS="phpunit/phpunit" - - php: 7 - env: - - DEPS=latest - php: 7.1 env: - DEPS=lowest @@ -58,8 +38,7 @@ before_install: - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi install: - - travis_retry composer install $COMPOSER_ARGS --ignore-platform-reqs - - if [[ $LEGACY_DEPS != '' ]]; then travis_retry composer update $COMPOSER_ARGS --with-dependencies $LEGACY_DEPS ; fi + - travis_retry composer install $COMPOSER_ARGS - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable $COMPOSER_ARGS ; fi - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $COVERAGE_DEPS ; fi diff --git a/README.md b/README.md index b68a47d..17225bb 100644 --- a/README.md +++ b/README.md @@ -18,5 +18,20 @@ request and responses, and provides capabilities around: Additionally, it supports combinations of different route types in tree structures, allowing for fast, b-tree lookups. -- File issues at https://github.com/zendframework/zend-router/issues -- Documentation is at https://docs.zendframework.com/zend-router +## Installation + +Run the following to install this library: + +```bash +$ composer require zendframework/zend-router +``` + +## Documentation + +Documentation is [in the doc tree](docs/book/), and can be compiled using [mkdocs](http://www.mkdocs.org): + +```bash +$ mkdocs build +``` + +You may also [browse the documentation online](https://docs.zendframework.com/zend-router/). diff --git a/composer.json b/composer.json index 461559c..ffae56d 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,6 @@ "zf", "zend", "zendframework", - "mvc", "routing" ], "support": { @@ -18,14 +17,14 @@ "forum": "https://discourse.zendframework.com/c/questions/components" }, "require": { - "php": "^5.6 || ^7.0", + "php": "^7.1", "container-interop/container-interop": "^1.2", "zendframework/zend-http": "^2.6", - "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", - "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + "zendframework/zend-servicemanager": "^3.3", + "zendframework/zend-stdlib": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^5.7.22 || ^6.4.1", + "phpunit/phpunit": "^7.0", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-i18n": "^2.7.4" }, diff --git a/composer.lock b/composer.lock index a09c00a..a4ce3da 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "8624751621bbfc18a2c1dd41b0441e19", + "content-hash": "99cd27be0520efe05a2974f3fd6996eb", "packages": [ { "name": "container-interop/container-interop", @@ -132,35 +132,35 @@ }, { "name": "zendframework/zend-http", - "version": "2.6.0", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-http.git", - "reference": "09f4d279f46d86be63171ff62ee0f79eca878678" + "reference": "78aa510c0ea64bfb2aa234f50c4f232c9531acfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-http/zipball/09f4d279f46d86be63171ff62ee0f79eca878678", - "reference": "09f4d279f46d86be63171ff62ee0f79eca878678", + "url": "https://api.github.com/repos/zendframework/zend-http/zipball/78aa510c0ea64bfb2aa234f50c4f232c9531acfa", + "reference": "78aa510c0ea64bfb2aa234f50c4f232c9531acfa", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-loader": "^2.5", - "zendframework/zend-stdlib": "^2.5 || ^3.0", - "zendframework/zend-uri": "^2.5", - "zendframework/zend-validator": "^2.5" + "php": "^5.6 || ^7.0", + "zendframework/zend-loader": "^2.5.1", + "zendframework/zend-stdlib": "^3.1 || ^2.7.7", + "zendframework/zend-uri": "^2.5.2", + "zendframework/zend-validator": "^2.10.1" }, "require-dev": { - "phpunit/phpunit": "^4.0", + "phpunit/phpunit": "^6.4.1 || ^5.7.15", "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-config": "^2.5" + "zendframework/zend-config": "^3.1 || ^2.6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev", - "dev-develop": "2.7-dev" + "dev-master": "2.7-dev", + "dev-develop": "2.8-dev" } }, "autoload": { @@ -175,10 +175,13 @@ "description": "provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests", "homepage": "https://github.com/zendframework/zend-http", "keywords": [ + "ZendFramework", "http", - "zf2" + "http client", + "zend", + "zf" ], - "time": "2017-01-31T14:41:02+00:00" + "time": "2017-10-13T12:06:24+00:00" }, { "name": "zendframework/zend-loader", @@ -226,16 +229,16 @@ }, { "name": "zendframework/zend-servicemanager", - "version": "3.3.0", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-servicemanager.git", - "reference": "c3036efb81f71bfa36cc9962ee5d4474f36581d0" + "reference": "9f35a104b8d4d3b32da5f4a3b6efc0dd62e5af42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/c3036efb81f71bfa36cc9962ee5d4474f36581d0", - "reference": "c3036efb81f71bfa36cc9962ee5d4474f36581d0", + "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/9f35a104b8d4d3b32da5f4a3b6efc0dd62e5af42", + "reference": "9f35a104b8d4d3b32da5f4a3b6efc0dd62e5af42", "shasum": "" }, "require": { @@ -249,10 +252,10 @@ "psr/container-implementation": "^1.0" }, "require-dev": { - "mikey179/vfsstream": "^1.6", + "mikey179/vfsstream": "^1.6.5", "ocramius/proxy-manager": "^1.0 || ^2.0", - "phpbench/phpbench": "^0.10.0", - "phpunit/phpunit": "^5.7 || ^6.0.6", + "phpbench/phpbench": "^0.13.0", + "phpunit/phpunit": "^5.7.25 || ^6.4.4", "zendframework/zend-coding-standard": "~1.0.0" }, "suggest": { @@ -267,7 +270,7 @@ "extra": { "branch-alias": { "dev-master": "3.3-dev", - "dev-develop": "3.4-dev" + "dev-develop": "4.0-dev" } }, "autoload": { @@ -279,13 +282,18 @@ "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-servicemanager", + "description": "Factory-Driven Dependency Injection Container", "keywords": [ + "PSR-11", + "ZendFramework", + "dependency-injection", + "di", + "dic", "service-manager", "servicemanager", "zf" ], - "time": "2017-03-01T22:08:02+00:00" + "time": "2018-01-29T16:48:37+00:00" }, { "name": "zendframework/zend-stdlib", @@ -381,16 +389,16 @@ }, { "name": "zendframework/zend-validator", - "version": "2.10.1", + "version": "2.10.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-validator.git", - "reference": "010084ddbd33299bf51ea6f0e07f8f4e8bd832a8" + "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/010084ddbd33299bf51ea6f0e07f8f4e8bd832a8", - "reference": "010084ddbd33299bf51ea6f0e07f8f4e8bd832a8", + "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9", + "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9", "shasum": "" }, "require": { @@ -425,8 +433,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10-dev", - "dev-develop": "2.11-dev" + "dev-master": "2.10.x-dev", + "dev-develop": "2.11.x-dev" }, "zf": { "component": "Zend\\Validator", @@ -448,7 +456,7 @@ "validator", "zf2" ], - "time": "2017-08-22T14:19:23+00:00" + "time": "2018-02-01T17:05:33+00:00" } ], "packages-dev": [ @@ -508,37 +516,40 @@ }, { "name": "myclabs/deep-copy", - "version": "1.6.1", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": "^5.6 || ^7.0" }, "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" }, "type": "library", "autoload": { "psr-4": { "DeepCopy\\": "src/DeepCopy/" - } + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", "keywords": [ "clone", "copy", @@ -546,7 +557,7 @@ "object", "object graph" ], - "time": "2017-04-12T18:52:22+00:00" + "time": "2017-10-19T19:58:43+00:00" }, { "name": "phar-io/manifest", @@ -706,29 +717,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.1.1", + "version": "4.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2" + "reference": "94fd0001232e47129dd3504189fa1c7225010d08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2d3d238c433cf69caeb4842e97a3223a116f94b2", - "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08", "shasum": "" }, "require": { "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/reflection-common": "^1.0.0", "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, "autoload": { "psr-4": { "phpDocumentor\\Reflection\\": [ @@ -747,7 +764,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-30T18:51:59+00:00" + "time": "2017-11-30T07:14:17+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -798,16 +815,16 @@ }, { "name": "phpspec/prophecy", - "version": "v1.7.2", + "version": "1.7.3", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" + "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", + "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", "shasum": "" }, "require": { @@ -819,7 +836,7 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7" }, "type": "library", "extra": { @@ -857,45 +874,44 @@ "spy", "stub" ], - "time": "2017-09-04T11:05:03+00:00" + "time": "2017-11-24T13:59:53+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "5.2.2", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b" + "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/8ed1902a57849e117b5651fc1a5c48110946c06b", - "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f8ca4b604baf23dab89d87773c28cc07405189ba", + "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.0", + "php": "^7.1", "phpunit/php-file-iterator": "^1.4.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^1.4.11 || ^2.0", + "phpunit/php-token-stream": "^3.0", "sebastian/code-unit-reverse-lookup": "^1.0.1", "sebastian/environment": "^3.0", "sebastian/version": "^2.0.1", "theseer/tokenizer": "^1.1" }, "require-dev": { - "ext-xdebug": "^2.5", - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^7.0" }, "suggest": { - "ext-xdebug": "^2.5.5" + "ext-xdebug": "^2.6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.2.x-dev" + "dev-master": "6.0-dev" } }, "autoload": { @@ -910,7 +926,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -921,20 +937,20 @@ "testing", "xunit" ], - "time": "2017-08-03T12:40:43+00:00" + "time": "2018-02-02T07:01:41+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.2", + "version": "1.4.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", "shasum": "" }, "require": { @@ -968,7 +984,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "time": "2017-11-27T13:52:08+00:00" }, { "name": "phpunit/php-text-template", @@ -1013,28 +1029,28 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.9", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f", + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1049,7 +1065,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -1058,33 +1074,33 @@ "keywords": [ "timer" ], - "time": "2017-02-26T11:10:40+00:00" + "time": "2018-02-01T13:07:23+00:00" }, { "name": "phpunit/php-token-stream", - "version": "2.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0" + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0", - "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/21ad88bbba7c3d93530d93994e0a33cd45f02ace", + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2.4" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1107,20 +1123,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-08-20T05:47:52+00:00" + "time": "2018-02-01T13:16:43+00:00" }, { "name": "phpunit/phpunit", - "version": "6.4.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b770d8ba7e60295ee91d69d5a5e01ae833cac220" + "reference": "9b3373439fdf2f3e9d1578f5e408a3a0d161c3bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b770d8ba7e60295ee91d69d5a5e01ae833cac220", - "reference": "b770d8ba7e60295ee91d69d5a5e01ae833cac220", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b3373439fdf2f3e9d1578f5e408a3a0d161c3bc", + "reference": "9b3373439fdf2f3e9d1578f5e408a3a0d161c3bc", "shasum": "" }, "require": { @@ -1132,15 +1148,15 @@ "myclabs/deep-copy": "^1.6.1", "phar-io/manifest": "^1.0.1", "phar-io/version": "^1.0", - "php": "^7.0", + "php": "^7.1", "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.2.2", - "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-code-coverage": "^6.0", + "phpunit/php-file-iterator": "^1.4.3", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^4.0.3", - "sebastian/comparator": "^2.0.2", - "sebastian/diff": "^2.0", + "phpunit/php-timer": "^2.0", + "phpunit/phpunit-mock-objects": "^6.0", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^3.0", "sebastian/environment": "^3.1", "sebastian/exporter": "^3.1", "sebastian/global-state": "^2.0", @@ -1148,16 +1164,12 @@ "sebastian/resource-operations": "^1.0", "sebastian/version": "^2.0.1" }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2", - "phpunit/dbunit": "<3.0" - }, "require-dev": { "ext-pdo": "*" }, "suggest": { "ext-xdebug": "*", - "phpunit/php-invoker": "^1.1" + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -1165,7 +1177,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.4.x-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -1191,33 +1203,30 @@ "testing", "xunit" ], - "time": "2017-10-07T17:53:53+00:00" + "time": "2018-02-02T05:04:08+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "4.0.4", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "2f789b59ab89669015ad984afa350c4ec577ade0" + "reference": "e495e5d3660321b62c294d8c0e954d02d6ce2573" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/2f789b59ab89669015ad984afa350c4ec577ade0", - "reference": "2f789b59ab89669015ad984afa350c4ec577ade0", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/e495e5d3660321b62c294d8c0e954d02d6ce2573", + "reference": "e495e5d3660321b62c294d8c0e954d02d6ce2573", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.5", - "php": "^7.0", + "php": "^7.1", "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.0" - }, - "conflict": { - "phpunit/phpunit": "<6.0" + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^7.0" }, "suggest": { "ext-soap": "*" @@ -1225,7 +1234,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "6.0.x-dev" } }, "autoload": { @@ -1240,7 +1249,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -1250,7 +1259,7 @@ "mock", "xunit" ], - "time": "2017-08-03T14:08:16+00:00" + "time": "2018-02-01T13:11:13+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1299,30 +1308,30 @@ }, { "name": "sebastian/comparator", - "version": "2.0.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a" + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/ae068fede81d06e7bb9bb46a367210a3d3e1fe6a", - "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", "shasum": "" }, "require": { "php": "^7.0", - "sebastian/diff": "^2.0", - "sebastian/exporter": "^3.0" + "sebastian/diff": "^2.0 || ^3.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.1.x-dev" } }, "autoload": { @@ -1353,38 +1362,39 @@ } ], "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", "equality" ], - "time": "2017-08-03T07:14:59+00:00" + "time": "2018-02-01T13:46:46+00:00" }, { "name": "sebastian/diff", - "version": "2.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/e09160918c66281713f1c324c1f4c4c3037ba1e8", + "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "phpunit/phpunit": "^7.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1409,9 +1419,12 @@ "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "diff" + "diff", + "udiff", + "unidiff", + "unified diff" ], - "time": "2017-08-03T08:09:46+00:00" + "time": "2018-02-01T13:45:15+00:00" }, { "name": "sebastian/environment", @@ -1931,16 +1944,16 @@ }, { "name": "webmozart/assert", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + "reference": "0df1908962e7a3071564e857d86874dad1ef204a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a", "shasum": "" }, "require": { @@ -1977,7 +1990,7 @@ "check", "validate" ], - "time": "2016-11-23T20:04:58+00:00" + "time": "2018-01-29T19:49:41+00:00" }, { "name": "zendframework/zend-coding-standard", @@ -2082,7 +2095,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "platform-dev": [] } From eecaa4fdb41ac831194b299276da74eb1b627c83 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 3 Feb 2018 14:18:01 +1000 Subject: [PATCH 02/47] Add strict types declaration --- src/ConfigProvider.php | 2 ++ src/Exception/ExceptionInterface.php | 2 ++ src/Exception/InvalidArgumentException.php | 2 ++ src/Exception/RuntimeException.php | 2 ++ src/Http/Chain.php | 2 ++ src/Http/Hostname.php | 2 ++ src/Http/HttpRouterFactory.php | 2 ++ src/Http/Literal.php | 2 ++ src/Http/Method.php | 2 ++ src/Http/Part.php | 2 ++ src/Http/Regex.php | 2 ++ src/Http/RouteInterface.php | 2 ++ src/Http/RouteMatch.php | 2 ++ src/Http/Scheme.php | 2 ++ src/Http/Segment.php | 2 ++ src/Http/TranslatorAwareTreeRouteStack.php | 2 ++ src/Http/TreeRouteStack.php | 2 ++ src/Http/Wildcard.php | 2 ++ src/Module.php | 2 ++ src/PriorityList.php | 2 ++ src/RouteInterface.php | 2 ++ src/RouteInvokableFactory.php | 2 ++ src/RouteMatch.php | 2 ++ src/RoutePluginManager.php | 2 ++ src/RoutePluginManagerFactory.php | 2 ++ src/RouteStackInterface.php | 2 ++ src/RouterConfigTrait.php | 2 ++ src/RouterFactory.php | 2 ++ src/SimpleRouteStack.php | 2 ++ test/FactoryTester.php | 2 ++ test/Http/ChainTest.php | 2 ++ test/Http/HostnameTest.php | 2 ++ test/Http/HttpRouterFactoryTest.php | 2 ++ test/Http/LiteralTest.php | 2 ++ test/Http/MethodTest.php | 2 ++ test/Http/PartTest.php | 2 ++ test/Http/RegexTest.php | 2 ++ test/Http/RouteMatchTest.php | 2 ++ test/Http/SchemeTest.php | 2 ++ test/Http/SegmentTest.php | 2 ++ test/Http/TestAsset/DummyRoute.php | 2 ++ test/Http/TestAsset/DummyRouteWithParam.php | 2 ++ .../TranslatorAwareTreeRouteStackTest.php | 20 ++++++++++--------- test/Http/TreeRouteStackTest.php | 2 ++ test/Http/WildcardTest.php | 2 ++ test/Http/_files/tokens.de.php | 3 +++ test/Http/_files/tokens.en.php | 3 +++ test/PriorityListTest.php | 2 ++ test/RouteMatchTest.php | 2 ++ test/RoutePluginManagerFactoryTest.php | 2 ++ test/RoutePluginManagerTest.php | 2 ++ test/RouterFactoryTest.php | 2 ++ test/SimpleRouteStackTest.php | 2 ++ test/TestAsset/DummyRoute.php | 2 ++ test/TestAsset/DummyRouteWithParam.php | 2 ++ test/TestAsset/Router.php | 2 ++ 56 files changed, 123 insertions(+), 9 deletions(-) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index a9e4596..f4801c5 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; /** diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index df8da25..314dedd 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Exception; interface ExceptionInterface diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 6b390fa..5c981fc 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Exception; class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index d6b2b10..3ac6d9e 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Exception; class RuntimeException extends \RuntimeException implements ExceptionInterface diff --git a/src/Http/Chain.php b/src/Http/Chain.php index 515b022..4a7b384 100644 --- a/src/Http/Chain.php +++ b/src/Http/Chain.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use ArrayObject; diff --git a/src/Http/Hostname.php b/src/Http/Hostname.php index 5ddd9f5..b2e2b7b 100644 --- a/src/Http/Hostname.php +++ b/src/Http/Hostname.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Traversable; diff --git a/src/Http/HttpRouterFactory.php b/src/Http/HttpRouterFactory.php index ffcd658..d2f4c72 100644 --- a/src/Http/HttpRouterFactory.php +++ b/src/Http/HttpRouterFactory.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Interop\Container\ContainerInterface; diff --git a/src/Http/Literal.php b/src/Http/Literal.php index 28a8c20..7aacfd1 100644 --- a/src/Http/Literal.php +++ b/src/Http/Literal.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Traversable; diff --git a/src/Http/Method.php b/src/Http/Method.php index 69c83a5..454cdf6 100644 --- a/src/Http/Method.php +++ b/src/Http/Method.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Traversable; diff --git a/src/Http/Part.php b/src/Http/Part.php index 2ea728d..e1e2fe8 100644 --- a/src/Http/Part.php +++ b/src/Http/Part.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use ArrayObject; diff --git a/src/Http/Regex.php b/src/Http/Regex.php index 7b2ee46..561f33b 100644 --- a/src/Http/Regex.php +++ b/src/Http/Regex.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Traversable; diff --git a/src/Http/RouteInterface.php b/src/Http/RouteInterface.php index fb7fdf7..18a833d 100644 --- a/src/Http/RouteInterface.php +++ b/src/Http/RouteInterface.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Zend\Router\RouteInterface as BaseRoute; diff --git a/src/Http/RouteMatch.php b/src/Http/RouteMatch.php index 57d9ac9..d3dbbfc 100644 --- a/src/Http/RouteMatch.php +++ b/src/Http/RouteMatch.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Zend\Router\RouteMatch as BaseRouteMatch; diff --git a/src/Http/Scheme.php b/src/Http/Scheme.php index 1007343..463050e 100644 --- a/src/Http/Scheme.php +++ b/src/Http/Scheme.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Traversable; diff --git a/src/Http/Segment.php b/src/Http/Segment.php index 01ee076..a9c8295 100644 --- a/src/Http/Segment.php +++ b/src/Http/Segment.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Traversable; diff --git a/src/Http/TranslatorAwareTreeRouteStack.php b/src/Http/TranslatorAwareTreeRouteStack.php index 7249c62..87a45cf 100644 --- a/src/Http/TranslatorAwareTreeRouteStack.php +++ b/src/Http/TranslatorAwareTreeRouteStack.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Zend\I18n\Translator\TranslatorInterface as Translator; diff --git a/src/Http/TreeRouteStack.php b/src/Http/TreeRouteStack.php index 3f99e48..a576ec8 100644 --- a/src/Http/TreeRouteStack.php +++ b/src/Http/TreeRouteStack.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use ArrayObject; diff --git a/src/Http/Wildcard.php b/src/Http/Wildcard.php index a56f3b7..f7b797b 100644 --- a/src/Http/Wildcard.php +++ b/src/Http/Wildcard.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router\Http; use Traversable; diff --git a/src/Module.php b/src/Module.php index cbe86f9..5d39123 100644 --- a/src/Module.php +++ b/src/Module.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; /** diff --git a/src/PriorityList.php b/src/PriorityList.php index 1cd7b55..74d916a 100644 --- a/src/PriorityList.php +++ b/src/PriorityList.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; use Zend\Stdlib\PriorityList as StdlibPriorityList; diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 72266e0..eb96b75 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; use Zend\Stdlib\RequestInterface as Request; diff --git a/src/RouteInvokableFactory.php b/src/RouteInvokableFactory.php index 16a2dce..bab50a5 100644 --- a/src/RouteInvokableFactory.php +++ b/src/RouteInvokableFactory.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; use Interop\Container\ContainerInterface; diff --git a/src/RouteMatch.php b/src/RouteMatch.php index 471c101..439aa14 100644 --- a/src/RouteMatch.php +++ b/src/RouteMatch.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; /** diff --git a/src/RoutePluginManager.php b/src/RoutePluginManager.php index 6f42149..06c809f 100644 --- a/src/RoutePluginManager.php +++ b/src/RoutePluginManager.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; use Interop\Container\ContainerInterface; diff --git a/src/RoutePluginManagerFactory.php b/src/RoutePluginManagerFactory.php index 1a4197b..99a49d4 100644 --- a/src/RoutePluginManagerFactory.php +++ b/src/RoutePluginManagerFactory.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; use Interop\Container\ContainerInterface; diff --git a/src/RouteStackInterface.php b/src/RouteStackInterface.php index 8d90dd3..9461b4b 100644 --- a/src/RouteStackInterface.php +++ b/src/RouteStackInterface.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; interface RouteStackInterface extends RouteInterface diff --git a/src/RouterConfigTrait.php b/src/RouterConfigTrait.php index 7420d84..0e20e9c 100644 --- a/src/RouterConfigTrait.php +++ b/src/RouterConfigTrait.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; use Interop\Container\ContainerInterface; diff --git a/src/RouterFactory.php b/src/RouterFactory.php index 5fdd529..6179f00 100644 --- a/src/RouterFactory.php +++ b/src/RouterFactory.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; use Interop\Container\ContainerInterface; diff --git a/src/SimpleRouteStack.php b/src/SimpleRouteStack.php index 8175064..3794ef0 100644 --- a/src/SimpleRouteStack.php +++ b/src/SimpleRouteStack.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace Zend\Router; use Traversable; diff --git a/test/FactoryTester.php b/test/FactoryTester.php index f3f600e..dc00eb4 100644 --- a/test/FactoryTester.php +++ b/test/FactoryTester.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router; use ArrayIterator; diff --git a/test/Http/ChainTest.php b/test/Http/ChainTest.php index 799fc72..843e507 100644 --- a/test/Http/ChainTest.php +++ b/test/Http/ChainTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/HostnameTest.php b/test/Http/HostnameTest.php index dde8190..56a1f56 100644 --- a/test/Http/HostnameTest.php +++ b/test/Http/HostnameTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/HttpRouterFactoryTest.php b/test/Http/HttpRouterFactoryTest.php index de749fc..ff546d6 100644 --- a/test/Http/HttpRouterFactoryTest.php +++ b/test/Http/HttpRouterFactoryTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use Zend\Router\Http\HttpRouterFactory; diff --git a/test/Http/LiteralTest.php b/test/Http/LiteralTest.php index 8fbb538..63cadc9 100644 --- a/test/Http/LiteralTest.php +++ b/test/Http/LiteralTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/MethodTest.php b/test/Http/MethodTest.php index 6836238..80a2514 100644 --- a/test/Http/MethodTest.php +++ b/test/Http/MethodTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/PartTest.php b/test/Http/PartTest.php index 74351d5..26da4fb 100644 --- a/test/Http/PartTest.php +++ b/test/Http/PartTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use ArrayObject; diff --git a/test/Http/RegexTest.php b/test/Http/RegexTest.php index c166749..1509842 100644 --- a/test/Http/RegexTest.php +++ b/test/Http/RegexTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/RouteMatchTest.php b/test/Http/RouteMatchTest.php index 976bec1..d20f116 100644 --- a/test/Http/RouteMatchTest.php +++ b/test/Http/RouteMatchTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/SchemeTest.php b/test/Http/SchemeTest.php index 233418b..b94b427 100644 --- a/test/Http/SchemeTest.php +++ b/test/Http/SchemeTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/SegmentTest.php b/test/Http/SegmentTest.php index 4275055..b81b65c 100644 --- a/test/Http/SegmentTest.php +++ b/test/Http/SegmentTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/TestAsset/DummyRoute.php b/test/Http/TestAsset/DummyRoute.php index 796e880..cca7766 100644 --- a/test/Http/TestAsset/DummyRoute.php +++ b/test/Http/TestAsset/DummyRoute.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http\TestAsset; use Zend\Router\Http\RouteInterface; diff --git a/test/Http/TestAsset/DummyRouteWithParam.php b/test/Http/TestAsset/DummyRouteWithParam.php index 6d5d6d8..00e6b46 100644 --- a/test/Http/TestAsset/DummyRouteWithParam.php +++ b/test/Http/TestAsset/DummyRouteWithParam.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http\TestAsset; use Zend\Router\Http\RouteMatch; diff --git a/test/Http/TranslatorAwareTreeRouteStackTest.php b/test/Http/TranslatorAwareTreeRouteStackTest.php index b95ca0d..03eba53 100644 --- a/test/Http/TranslatorAwareTreeRouteStackTest.php +++ b/test/Http/TranslatorAwareTreeRouteStackTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; @@ -103,11 +105,11 @@ public function testTranslatorIsPassedThroughMatchMethod() $route = $this->getMock(RouteInterface::class); $route->expects($this->once()) ->method('match') - ->with( - $this->equalTo($request), - $this->isNull(), - $this->equalTo(['translator' => $translator, 'text_domain' => 'default']) - ); + ->with( + $this->equalTo($request), + $this->isNull(), + $this->equalTo(['translator' => $translator, 'text_domain' => 'default']) + ); $stack = new TranslatorAwareTreeRouteStack(); $stack->addRoute('test', $route); @@ -123,10 +125,10 @@ public function testTranslatorIsPassedThroughAssembleMethod() $route = $this->getMock(RouteInterface::class); $route->expects($this->once()) ->method('assemble') - ->with( - $this->equalTo([]), - $this->equalTo(['translator' => $translator, 'text_domain' => 'default', 'uri' => $uri]) - ); + ->with( + $this->equalTo([]), + $this->equalTo(['translator' => $translator, 'text_domain' => 'default', 'uri' => $uri]) + ); $stack = new TranslatorAwareTreeRouteStack(); $stack->addRoute('test', $route); diff --git a/test/Http/TreeRouteStackTest.php b/test/Http/TreeRouteStackTest.php index ac8fb4f..85a0036 100644 --- a/test/Http/TreeRouteStackTest.php +++ b/test/Http/TreeRouteStackTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use ArrayIterator; diff --git a/test/Http/WildcardTest.php b/test/Http/WildcardTest.php index a766868..398eca6 100644 --- a/test/Http/WildcardTest.php +++ b/test/Http/WildcardTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\Http; use PHPUnit\Framework\TestCase; diff --git a/test/Http/_files/tokens.de.php b/test/Http/_files/tokens.de.php index b6ca98d..3d7157d 100644 --- a/test/Http/_files/tokens.de.php +++ b/test/Http/_files/tokens.de.php @@ -1,4 +1,7 @@ 'hauptseite', ]; diff --git a/test/Http/_files/tokens.en.php b/test/Http/_files/tokens.en.php index 02cdd7e..f3243c4 100644 --- a/test/Http/_files/tokens.en.php +++ b/test/Http/_files/tokens.en.php @@ -1,4 +1,7 @@ 'homepage', ]; diff --git a/test/PriorityListTest.php b/test/PriorityListTest.php index 9c5d61d..31af050 100644 --- a/test/PriorityListTest.php +++ b/test/PriorityListTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router; use PHPUnit\Framework\TestCase; diff --git a/test/RouteMatchTest.php b/test/RouteMatchTest.php index db6b9d2..99f0233 100644 --- a/test/RouteMatchTest.php +++ b/test/RouteMatchTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router; use PHPUnit\Framework\TestCase; diff --git a/test/RoutePluginManagerFactoryTest.php b/test/RoutePluginManagerFactoryTest.php index 8e15c02..5e07ded 100644 --- a/test/RoutePluginManagerFactoryTest.php +++ b/test/RoutePluginManagerFactoryTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router; use Interop\Container\ContainerInterface; diff --git a/test/RoutePluginManagerTest.php b/test/RoutePluginManagerTest.php index a0716b5..a911bca 100644 --- a/test/RoutePluginManagerTest.php +++ b/test/RoutePluginManagerTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router; use PHPUnit\Framework\TestCase; diff --git a/test/RouterFactoryTest.php b/test/RouterFactoryTest.php index 362a4ed..29473cc 100644 --- a/test/RouterFactoryTest.php +++ b/test/RouterFactoryTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router; use Interop\Container\ContainerInterface; diff --git a/test/SimpleRouteStackTest.php b/test/SimpleRouteStackTest.php index f21ab24..54ad654 100644 --- a/test/SimpleRouteStackTest.php +++ b/test/SimpleRouteStackTest.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router; use ArrayIterator; diff --git a/test/TestAsset/DummyRoute.php b/test/TestAsset/DummyRoute.php index 7b80254..c6d3ede 100644 --- a/test/TestAsset/DummyRoute.php +++ b/test/TestAsset/DummyRoute.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\TestAsset; use Zend\Router\RouteInterface; diff --git a/test/TestAsset/DummyRouteWithParam.php b/test/TestAsset/DummyRouteWithParam.php index 725e878..b3a3994 100644 --- a/test/TestAsset/DummyRouteWithParam.php +++ b/test/TestAsset/DummyRouteWithParam.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\TestAsset; use Zend\Router\RouteMatch; diff --git a/test/TestAsset/Router.php b/test/TestAsset/Router.php index 20babe9..a8c5e3b 100644 --- a/test/TestAsset/Router.php +++ b/test/TestAsset/Router.php @@ -5,6 +5,8 @@ * @license http://framework.zend.com/license/new-bsd New BSD License */ +declare(strict_types=1); + namespace ZendTest\Router\TestAsset; use Zend\Router\RouteStackInterface; From 0fd81c36fa67f0d1898995467fa8e42b539ae59b Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 18 Nov 2017 11:41:32 +1000 Subject: [PATCH 03/47] Remove unneeded composer conflict --- composer.json | 3 --- composer.lock | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/composer.json b/composer.json index ffae56d..5c1d4c7 100644 --- a/composer.json +++ b/composer.json @@ -28,9 +28,6 @@ "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-i18n": "^2.7.4" }, - "conflict": { - "zendframework/zend-mvc": "<3.0.0" - }, "suggest": { "zendframework/zend-i18n": "^2.7.4, if defining translatable HTTP path segments" }, diff --git a/composer.lock b/composer.lock index a4ce3da..8ed4fcf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "99cd27be0520efe05a2974f3fd6996eb", + "content-hash": "0ad35e8e2e010f015d8171ce19b2e8e1", "packages": [ { "name": "container-interop/container-interop", From 055d799013423fb8d5469d75b2c8b68c84c1072b Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 12 Oct 2017 17:59:46 +1000 Subject: [PATCH 04/47] Fix or suppress type errors --- src/Http/Literal.php | 2 +- src/Http/Regex.php | 2 +- src/Http/Segment.php | 6 +++--- src/Http/TreeRouteStack.php | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Http/Literal.php b/src/Http/Literal.php index 7aacfd1..4fe2ac6 100644 --- a/src/Http/Literal.php +++ b/src/Http/Literal.php @@ -93,7 +93,7 @@ public function match(Request $request, $pathOffset = null) $path = $uri->getPath(); if ($pathOffset !== null) { - if ($pathOffset >= 0 && strlen($path) >= $pathOffset && ! empty($this->route)) { + if ($pathOffset >= 0 && strlen((string) $path) >= $pathOffset && ! empty($this->route)) { if (strpos($path, $this->route, $pathOffset) === $pathOffset) { return new RouteMatch($this->defaults, strlen($this->route)); } diff --git a/src/Http/Regex.php b/src/Http/Regex.php index 561f33b..99f9840 100644 --- a/src/Http/Regex.php +++ b/src/Http/Regex.php @@ -114,7 +114,7 @@ public function match(Request $request, $pathOffset = null) $path = $uri->getPath(); if ($pathOffset !== null) { - $result = preg_match('(\G' . $this->regex . ')', $path, $matches, null, $pathOffset); + $result = preg_match('(\G' . $this->regex . ')', $path, $matches, 0, $pathOffset); } else { $result = preg_match('(^' . $this->regex . '$)', $path, $matches); } diff --git a/src/Http/Segment.php b/src/Http/Segment.php index a9c8295..4f88a14 100644 --- a/src/Http/Segment.php +++ b/src/Http/Segment.php @@ -320,7 +320,7 @@ protected function buildPath(array $parts, array $mergedParams, $isOptional, $ha $skip = false; } - $path .= $this->encode($mergedParams[$part[1]]); + $path .= $this->encode((string) $mergedParams[$part[1]]); $this->assembledParams[] = $part[1]; break; @@ -384,7 +384,7 @@ public function match(Request $request, $pathOffset = null, array $options = []) } if ($pathOffset !== null) { - $result = preg_match('(\G' . $regex . ')', $path, $matches, null, $pathOffset); + $result = preg_match('(\G' . $regex . ')', $path, $matches, 0, $pathOffset); } else { $result = preg_match('(^' . $regex . '$)', $path, $matches); } @@ -443,7 +443,7 @@ public function getAssembledParams() * @param string $value * @return string */ - protected function encode($value) + protected function encode(string $value) { $key = (string) $value; if (! isset(static::$cacheEncode[$key])) { diff --git a/src/Http/TreeRouteStack.php b/src/Http/TreeRouteStack.php index a576ec8..13bd5e2 100644 --- a/src/Http/TreeRouteStack.php +++ b/src/Http/TreeRouteStack.php @@ -298,7 +298,7 @@ public function match(Request $request, $pathOffset = null, array $options = []) } $uri = $request->getUri(); - $baseUrlLength = strlen($this->baseUrl) ?: null; + $baseUrlLength = strlen((string) $this->baseUrl) ?: null; if ($pathOffset !== null) { $baseUrlLength += $pathOffset; @@ -309,7 +309,7 @@ public function match(Request $request, $pathOffset = null, array $options = []) } if ($baseUrlLength !== null) { - $pathLength = strlen($uri->getPath()) - $baseUrlLength; + $pathLength = strlen((string) $uri->getPath()) - $baseUrlLength; } else { $pathLength = null; } From ec527bf694b6f019b60afbccb3f1f02020826afa Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 3 Feb 2018 14:31:15 +1000 Subject: [PATCH 05/47] Bumped to next dev version (4.0.0) --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f39e155..0155372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 4.0.0 - TBD + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + ## 3.1.0 - TBD ### Added From 61bf30d8ae822a1e8c1be68963cc86eef1044ac3 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 3 Feb 2018 14:34:40 +1000 Subject: [PATCH 06/47] Add CHANGELOG for #44 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0155372..6735491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ All notable changes to this project will be documented in this file, in reverse ### Removed -- Nothing. +- [#44](https://github.com/zendframework/zend-router/pull/44) removes support + for PHP versions prior to PHP 7.1. ### Fixed From d00b9d4e199570b97104a30ac5b396c543e042eb Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 18 Nov 2017 16:52:18 +1000 Subject: [PATCH 07/47] Be strict about unintentional coverage --- phpunit.xml.dist | 1 + test/Http/ChainTest.php | 3 +++ test/Http/HostnameTest.php | 3 +++ test/Http/LiteralTest.php | 3 +++ test/Http/MethodTest.php | 3 +++ test/Http/PartTest.php | 3 +++ test/Http/RegexTest.php | 3 +++ test/Http/SchemeTest.php | 3 +++ test/Http/SegmentTest.php | 3 +++ test/PriorityListTest.php | 3 +++ test/RoutePluginManagerFactoryTest.php | 3 +++ test/RoutePluginManagerTest.php | 3 +++ test/RouterFactoryTest.php | 3 +++ 13 files changed, 37 insertions(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 23f9287..e10e451 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,6 +2,7 @@ diff --git a/test/Http/ChainTest.php b/test/Http/ChainTest.php index 843e507..aa9b413 100644 --- a/test/Http/ChainTest.php +++ b/test/Http/ChainTest.php @@ -19,6 +19,9 @@ use Zend\ServiceManager\ServiceManager; use ZendTest\Router\FactoryTester; +/** + * @covers \Zend\Router\Route\Chain + */ class ChainTest extends TestCase { public static function getRoute() diff --git a/test/Http/HostnameTest.php b/test/Http/HostnameTest.php index 56a1f56..25fc09d 100644 --- a/test/Http/HostnameTest.php +++ b/test/Http/HostnameTest.php @@ -19,6 +19,9 @@ use Zend\Uri\Http as HttpUri; use ZendTest\Router\FactoryTester; +/** + * @covers \Zend\Router\Route\Hostname + */ class HostnameTest extends TestCase { public static function routeProvider() diff --git a/test/Http/LiteralTest.php b/test/Http/LiteralTest.php index 63cadc9..c4813de 100644 --- a/test/Http/LiteralTest.php +++ b/test/Http/LiteralTest.php @@ -16,6 +16,9 @@ use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; +/** + * @covers \Zend\Router\Route\Literal + */ class LiteralTest extends TestCase { public static function routeProvider() diff --git a/test/Http/MethodTest.php b/test/Http/MethodTest.php index 80a2514..848abd8 100644 --- a/test/Http/MethodTest.php +++ b/test/Http/MethodTest.php @@ -16,6 +16,9 @@ use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; +/** + * @covers \Zend\Router\Route\Method + */ class MethodTest extends TestCase { public static function routeProvider() diff --git a/test/Http/PartTest.php b/test/Http/PartTest.php index 26da4fb..8bab296 100644 --- a/test/Http/PartTest.php +++ b/test/Http/PartTest.php @@ -26,6 +26,9 @@ use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; +/** + * @covers \Zend\Router\Route\Part + */ class PartTest extends TestCase { public static function getRoutePlugins() diff --git a/test/Http/RegexTest.php b/test/Http/RegexTest.php index 1509842..a8a194f 100644 --- a/test/Http/RegexTest.php +++ b/test/Http/RegexTest.php @@ -16,6 +16,9 @@ use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; +/** + * @covers \Zend\Router\Route\Regex + */ class RegexTest extends TestCase { public static function routeProvider() diff --git a/test/Http/SchemeTest.php b/test/Http/SchemeTest.php index b94b427..88cae63 100644 --- a/test/Http/SchemeTest.php +++ b/test/Http/SchemeTest.php @@ -17,6 +17,9 @@ use Zend\Uri\Http as HttpUri; use ZendTest\Router\FactoryTester; +/** + * @covers \Zend\Router\Route\Scheme + */ class SchemeTest extends TestCase { public function testMatching() diff --git a/test/Http/SegmentTest.php b/test/Http/SegmentTest.php index b81b65c..06a1a3d 100644 --- a/test/Http/SegmentTest.php +++ b/test/Http/SegmentTest.php @@ -21,6 +21,9 @@ use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; +/** + * @covers \Zend\Router\Route\Segment + */ class SegmentTest extends TestCase { public function routeProvider() diff --git a/test/PriorityListTest.php b/test/PriorityListTest.php index 31af050..a7b5d09 100644 --- a/test/PriorityListTest.php +++ b/test/PriorityListTest.php @@ -12,6 +12,9 @@ use PHPUnit\Framework\TestCase; use Zend\Router\PriorityList; +/** + * @covers \Zend\Router\PriorityList + */ class PriorityListTest extends TestCase { /** diff --git a/test/RoutePluginManagerFactoryTest.php b/test/RoutePluginManagerFactoryTest.php index 5e07ded..1dd3eea 100644 --- a/test/RoutePluginManagerFactoryTest.php +++ b/test/RoutePluginManagerFactoryTest.php @@ -16,6 +16,9 @@ use Zend\Router\RoutePluginManagerFactory; use Zend\ServiceManager\ServiceLocatorInterface; +/** + * @covers \Zend\Router\RoutePluginManagerFactory + */ class RoutePluginManagerFactoryTest extends TestCase { public function setUp() diff --git a/test/RoutePluginManagerTest.php b/test/RoutePluginManagerTest.php index a911bca..b222b36 100644 --- a/test/RoutePluginManagerTest.php +++ b/test/RoutePluginManagerTest.php @@ -14,6 +14,9 @@ use Zend\ServiceManager\Exception\ServiceNotFoundException; use Zend\ServiceManager\ServiceManager; +/** + * @covers \Zend\Router\RoutePluginManager + */ class RoutePluginManagerTest extends TestCase { public function testLoadNonExistentRoute() diff --git a/test/RouterFactoryTest.php b/test/RouterFactoryTest.php index 29473cc..7574b44 100644 --- a/test/RouterFactoryTest.php +++ b/test/RouterFactoryTest.php @@ -17,6 +17,9 @@ use Zend\ServiceManager\Config; use Zend\ServiceManager\ServiceManager; +/** + * @covers \Zend\Router\RouterFactory + */ class RouterFactoryTest extends TestCase { public function setUp() From db83d16287274ce5dc5939429d5dedc3e3ad02fd Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 3 Feb 2018 15:35:01 +1000 Subject: [PATCH 08/47] Drop zend-http and add diactoros Diactoros should be replaced by PSR-17 implementation once it is accepted --- composer.json | 3 +- composer.lock | 253 ++++++++++---------------------------------------- 2 files changed, 50 insertions(+), 206 deletions(-) diff --git a/composer.json b/composer.json index 5c1d4c7..02f7e0a 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "require": { "php": "^7.1", "container-interop/container-interop": "^1.2", - "zendframework/zend-http": "^2.6", + "psr/http-message": "^1.0", + "zendframework/zend-diactoros": "^1.7", "zendframework/zend-servicemanager": "^3.3", "zendframework/zend-stdlib": "^3.1" }, diff --git a/composer.lock b/composer.lock index 8ed4fcf..fd22fa0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "0ad35e8e2e010f015d8171ce19b2e8e1", + "content-hash": "b20ef22f7463e54de613db8403c3daff", "packages": [ { "name": "container-interop/container-interop", @@ -87,145 +87,106 @@ "time": "2017-02-14T16:28:37+00:00" }, { - "name": "zendframework/zend-escaper", - "version": "2.5.2", + "name": "psr/http-message", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/zendframework/zend-escaper.git", - "reference": "2dcd14b61a72d8b8e27d579c6344e12c26141d4e" + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/2dcd14b61a72d8b8e27d579c6344e12c26141d4e", - "reference": "2dcd14b61a72d8b8e27d579c6344e12c26141d4e", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", "shasum": "" }, "require": { - "php": ">=5.5" - }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0" + "php": ">=5.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev", - "dev-develop": "2.6-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Zend\\Escaper\\": "src/" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "homepage": "https://github.com/zendframework/zend-escaper", - "keywords": [ - "escaper", - "zf2" + "MIT" ], - "time": "2016-06-30T19:48:38+00:00" - }, - { - "name": "zendframework/zend-http", - "version": "2.7.0", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-http.git", - "reference": "78aa510c0ea64bfb2aa234f50c4f232c9531acfa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-http/zipball/78aa510c0ea64bfb2aa234f50c4f232c9531acfa", - "reference": "78aa510c0ea64bfb2aa234f50c4f232c9531acfa", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0", - "zendframework/zend-loader": "^2.5.1", - "zendframework/zend-stdlib": "^3.1 || ^2.7.7", - "zendframework/zend-uri": "^2.5.2", - "zendframework/zend-validator": "^2.10.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.4.1 || ^5.7.15", - "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-config": "^3.1 || ^2.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Zend\\Http\\": "src/" + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } - }, - "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://github.com/zendframework/zend-http", + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", "keywords": [ - "ZendFramework", "http", - "http client", - "zend", - "zf" + "http-message", + "psr", + "psr-7", + "request", + "response" ], - "time": "2017-10-13T12:06:24+00:00" + "time": "2016-08-06T14:39:51+00:00" }, { - "name": "zendframework/zend-loader", - "version": "2.5.1", + "name": "zendframework/zend-diactoros", + "version": "1.7.0", "source": { "type": "git", - "url": "https://github.com/zendframework/zend-loader.git", - "reference": "c5fd2f071bde071f4363def7dea8dec7393e135c" + "url": "https://github.com/zendframework/zend-diactoros.git", + "reference": "ed6ce7e2105c400ca10277643a8327957c0384b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/c5fd2f071bde071f4363def7dea8dec7393e135c", - "reference": "c5fd2f071bde071f4363def7dea8dec7393e135c", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/ed6ce7e2105c400ca10277643a8327957c0384b7", + "reference": "ed6ce7e2105c400ca10277643a8327957c0384b7", "shasum": "" }, "require": { - "php": ">=5.3.23" + "php": "^5.6 || ^7.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0" + "ext-dom": "*", + "ext-libxml": "*", + "phpunit/phpunit": "^5.7.16 || ^6.0.8", + "zendframework/zend-coding-standard": "~1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev", - "dev-develop": "2.6-dev" + "dev-master": "1.7.x-dev", + "dev-develop": "1.8.x-dev" } }, "autoload": { "psr-4": { - "Zend\\Loader\\": "src/" + "Zend\\Diactoros\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "BSD-2-Clause" ], - "homepage": "https://github.com/zendframework/zend-loader", + "description": "PSR HTTP Message implementations", + "homepage": "https://github.com/zendframework/zend-diactoros", "keywords": [ - "loader", - "zf2" + "http", + "psr", + "psr-7" ], - "time": "2015-06-03T14:05:47+00:00" + "time": "2018-01-04T18:21:48+00:00" }, { "name": "zendframework/zend-servicemanager", @@ -339,124 +300,6 @@ "zf2" ], "time": "2016-09-13T14:38:50+00:00" - }, - { - "name": "zendframework/zend-uri", - "version": "2.5.2", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-uri.git", - "reference": "0bf717a239432b1a1675ae314f7c4acd742749ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/0bf717a239432b1a1675ae314f7c4acd742749ed", - "reference": "0bf717a239432b1a1675ae314f7c4acd742749ed", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-escaper": "^2.5", - "zendframework/zend-validator": "^2.5" - }, - "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev", - "dev-develop": "2.6-dev" - } - }, - "autoload": { - "psr-4": { - "Zend\\Uri\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "a component that aids in manipulating and validating » Uniform Resource Identifiers (URIs)", - "homepage": "https://github.com/zendframework/zend-uri", - "keywords": [ - "uri", - "zf2" - ], - "time": "2016-02-17T22:38:51+00:00" - }, - { - "name": "zendframework/zend-validator", - "version": "2.10.2", - "source": { - "type": "git", - "url": "https://github.com/zendframework/zend-validator.git", - "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9", - "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9", - "shasum": "" - }, - "require": { - "container-interop/container-interop": "^1.1", - "php": "^5.6 || ^7.0", - "zendframework/zend-stdlib": "^2.7.6 || ^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.0.8 || ^5.7.15", - "zendframework/zend-cache": "^2.6.1", - "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-config": "^2.6", - "zendframework/zend-db": "^2.7", - "zendframework/zend-filter": "^2.6", - "zendframework/zend-http": "^2.5.4", - "zendframework/zend-i18n": "^2.6", - "zendframework/zend-math": "^2.6", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-session": "^2.8", - "zendframework/zend-uri": "^2.5" - }, - "suggest": { - "zendframework/zend-db": "Zend\\Db component, required by the (No)RecordExists validator", - "zendframework/zend-filter": "Zend\\Filter component, required by the Digits validator", - "zendframework/zend-i18n": "Zend\\I18n component to allow translation of validation error messages", - "zendframework/zend-i18n-resources": "Translations of validator messages", - "zendframework/zend-math": "Zend\\Math component, required by the Csrf validator", - "zendframework/zend-servicemanager": "Zend\\ServiceManager component to allow using the ValidatorPluginManager and validator chains", - "zendframework/zend-session": "Zend\\Session component, ^2.8; required by the Csrf validator", - "zendframework/zend-uri": "Zend\\Uri component, required by the Uri and Sitemap\\Loc validators" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" - }, - "zf": { - "component": "Zend\\Validator", - "config-provider": "Zend\\Validator\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Zend\\Validator\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "provides a set of commonly needed validators", - "homepage": "https://github.com/zendframework/zend-validator", - "keywords": [ - "validator", - "zf2" - ], - "time": "2018-02-01T17:05:33+00:00" } ], "packages-dev": [ From 17c0468cf84497c3c8dc9e327a7a5cbbca906465 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 3 Feb 2018 16:13:48 +1000 Subject: [PATCH 09/47] Drop deprecated Wildcard route --- src/Http/TreeRouteStack.php | 6 -- src/Http/Wildcard.php | 194 ------------------------------------ test/Http/WildcardTest.php | 194 ------------------------------------ 3 files changed, 394 deletions(-) delete mode 100644 src/Http/Wildcard.php delete mode 100644 test/Http/WildcardTest.php diff --git a/src/Http/TreeRouteStack.php b/src/Http/TreeRouteStack.php index 13bd5e2..03ad319 100644 --- a/src/Http/TreeRouteStack.php +++ b/src/Http/TreeRouteStack.php @@ -107,10 +107,6 @@ protected function init() '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, @@ -121,7 +117,6 @@ protected function init() Regex::class => RouteInvokableFactory::class, Scheme::class => RouteInvokableFactory::class, Segment::class => RouteInvokableFactory::class, - Wildcard::class => RouteInvokableFactory::class, // v2 normalized names @@ -133,7 +128,6 @@ protected function init() 'zendmvcrouterhttpregex' => RouteInvokableFactory::class, 'zendmvcrouterhttpscheme' => RouteInvokableFactory::class, 'zendmvcrouterhttpsegment' => RouteInvokableFactory::class, - 'zendmvcrouterhttpwildcard' => RouteInvokableFactory::class, ], ]))->configureServiceManager($this->routePluginManager); } diff --git a/src/Http/Wildcard.php b/src/Http/Wildcard.php deleted file mode 100644 index f7b797b..0000000 --- a/src/Http/Wildcard.php +++ /dev/null @@ -1,194 +0,0 @@ -keyValueDelimiter = $keyValueDelimiter; - $this->paramDelimiter = $paramDelimiter; - $this->defaults = $defaults; - } - - /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $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 \Zend\Router\RouteInterface::match() - * @param Request $request - * @param integer|null $pathOffset - * @return RouteMatch|null - */ - public function match(Request $request, $pathOffset = null) - { - if (! method_exists($request, 'getUri')) { - return; - } - - $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; - } - - 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 \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @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) { - $elements[] = rawurlencode($key) . $this->keyValueDelimiter . rawurlencode($value); - - $this->assembledParams[] = $key; - } - - return $this->paramDelimiter . implode($this->paramDelimiter, $elements); - } - - return ''; - } - - /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see RouteInterface::getAssembledParams - * @return array - */ - public function getAssembledParams() - { - return $this->assembledParams; - } -} diff --git a/test/Http/WildcardTest.php b/test/Http/WildcardTest.php deleted file mode 100644 index 398eca6..0000000 --- a/test/Http/WildcardTest.php +++ /dev/null @@ -1,194 +0,0 @@ - [ - 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'] - ], - ]; - } - - /** - * @dataProvider routeProvider - * @param Wildcard $route - * @param string $path - * @param int $offset - * @param array $params - */ - 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)); - } - } - } - - /** - * @dataProvider routeProvider - * @param Wildcard $route - * @param string $path - * @param int $offset - * @param array $params - * @param boolean $skipAssembling - */ - 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. - return; - } - - $result = $route->assemble($params); - - if ($offset !== null) { - $this->assertEquals($offset, strpos($path, $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() - { - // @codingStandardsIgnoreStart - // 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`-=[]\\;\',./~!@#$%^&*()_+{}|:"<>?'; - // @codingStandardsIgnoreEnd - - $request = new Request(); - $request->setUri('http://example.com/foo/' . $in); - $route = new Wildcard(); - $match = $route->match($request); - - $this->assertSame($out, $match->getParam('foo')); - } -} From afa052883b617e568177433bbd2d38c2113f7a3b Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 3 Mar 2018 19:11:40 +1000 Subject: [PATCH 10/47] Move TreeRouteStack one namespace up --- src/ConfigProvider.php | 4 ++-- src/Http/Chain.php | 1 + src/Http/HttpRouterFactory.php | 1 + src/Http/Part.php | 1 + src/Http/TranslatorAwareTreeRouteStack.php | 1 + src/{Http => }/TreeRouteStack.php | 12 +++++++++++- test/{Http => }/TreeRouteStackTest.php | 5 +++-- 7 files changed, 20 insertions(+), 5 deletions(-) rename src/{Http => }/TreeRouteStack.php (97%) rename test/{Http => }/TreeRouteStackTest.php (99%) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index f4801c5..b3e3a17 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -41,13 +41,13 @@ public function getDependencyConfig() { return [ 'aliases' => [ - 'HttpRouter' => Http\TreeRouteStack::class, + 'HttpRouter' => TreeRouteStack::class, 'router' => RouteStackInterface::class, 'Router' => RouteStackInterface::class, 'RoutePluginManager' => RoutePluginManager::class, ], 'factories' => [ - Http\TreeRouteStack::class => Http\HttpRouterFactory::class, + TreeRouteStack::class => Http\HttpRouterFactory::class, RoutePluginManager::class => RoutePluginManagerFactory::class, RouteStackInterface::class => RouterFactory::class, ], diff --git a/src/Http/Chain.php b/src/Http/Chain.php index 4a7b384..f45c43f 100644 --- a/src/Http/Chain.php +++ b/src/Http/Chain.php @@ -14,6 +14,7 @@ use Zend\Router\Exception; use Zend\Router\PriorityList; use Zend\Router\RoutePluginManager; +use Zend\Router\TreeRouteStack; use Zend\Stdlib\ArrayUtils; use Zend\Stdlib\RequestInterface as Request; diff --git a/src/Http/HttpRouterFactory.php b/src/Http/HttpRouterFactory.php index d2f4c72..990d101 100644 --- a/src/Http/HttpRouterFactory.php +++ b/src/Http/HttpRouterFactory.php @@ -12,6 +12,7 @@ use Interop\Container\ContainerInterface; use Zend\Router\RouterConfigTrait; use Zend\Router\RouteStackInterface; +use Zend\Router\TreeRouteStack; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; diff --git a/src/Http/Part.php b/src/Http/Part.php index e1e2fe8..ca52b54 100644 --- a/src/Http/Part.php +++ b/src/Http/Part.php @@ -14,6 +14,7 @@ use Zend\Router\Exception; use Zend\Router\PriorityList; use Zend\Router\RoutePluginManager; +use Zend\Router\TreeRouteStack; use Zend\Stdlib\ArrayUtils; use Zend\Stdlib\RequestInterface as Request; diff --git a/src/Http/TranslatorAwareTreeRouteStack.php b/src/Http/TranslatorAwareTreeRouteStack.php index 87a45cf..5f95446 100644 --- a/src/Http/TranslatorAwareTreeRouteStack.php +++ b/src/Http/TranslatorAwareTreeRouteStack.php @@ -12,6 +12,7 @@ use Zend\I18n\Translator\TranslatorInterface as Translator; use Zend\I18n\Translator\TranslatorAwareInterface; use Zend\Router\Exception; +use Zend\Router\TreeRouteStack; use Zend\Stdlib\RequestInterface as Request; /** diff --git a/src/Http/TreeRouteStack.php b/src/TreeRouteStack.php similarity index 97% rename from src/Http/TreeRouteStack.php rename to src/TreeRouteStack.php index 03ad319..7ef3e5f 100644 --- a/src/Http/TreeRouteStack.php +++ b/src/TreeRouteStack.php @@ -7,11 +7,21 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router; use ArrayObject; use Traversable; use Zend\Router\Exception; +use Zend\Router\Http\Chain; +use Zend\Router\Http\Hostname; +use Zend\Router\Http\Literal; +use Zend\Router\Http\Method; +use Zend\Router\Http\Part; +use Zend\Router\Http\Regex; +use Zend\Router\Http\RouteInterface; +use Zend\Router\Http\RouteMatch; +use Zend\Router\Http\Scheme; +use Zend\Router\Http\Segment; use Zend\Router\RouteInvokableFactory; use Zend\Router\SimpleRouteStack; use Zend\ServiceManager\Config; diff --git a/test/Http/TreeRouteStackTest.php b/test/TreeRouteStackTest.php similarity index 99% rename from test/Http/TreeRouteStackTest.php rename to test/TreeRouteStackTest.php index 85a0036..74f74bc 100644 --- a/test/Http/TreeRouteStackTest.php +++ b/test/TreeRouteStackTest.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router; use ArrayIterator; use PHPUnit\Framework\TestCase; @@ -16,10 +16,11 @@ use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Exception\RuntimeException; use Zend\Router\Http\Hostname; -use Zend\Router\Http\TreeRouteStack; +use Zend\Router\TreeRouteStack; use Zend\Stdlib\Request as BaseRequest; use Zend\Uri\Http as HttpUri; use ZendTest\Router\FactoryTester; +use ZendTest\Router\Http\TestAsset; class TreeRouteStackTest extends TestCase { From 94408c8c7baa9bf1348dedd5e4a4a77ee6f5e360 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sun, 4 Mar 2018 12:34:59 +1000 Subject: [PATCH 11/47] Introduce RouteResult, deprecate RouteMatch RouteResult is an immutable value object representing routing result, succesfull or otherwise. RouteResult replaces RouteMatch, which is now deprecated. Routing failure due to HTTP method should produce RouteResult with method failure status and a list of allowed methods, which then can be used to produce 405 response. --- src/Exception/DomainException.php | 13 ++ src/PartialRouteResult.php | 271 ++++++++++++++++++++++++++++++ src/RouteMatch.php | 33 +++- src/RouteResult.php | 211 +++++++++++++++++++++++ test/PartialRouteResultTest.php | 267 +++++++++++++++++++++++++++++ test/RouteMatchTest.php | 21 +++ test/RouteResultTest.php | 169 +++++++++++++++++++ 7 files changed, 976 insertions(+), 9 deletions(-) create mode 100644 src/Exception/DomainException.php create mode 100644 src/PartialRouteResult.php create mode 100644 src/RouteResult.php create mode 100644 test/PartialRouteResultTest.php create mode 100644 test/RouteResultTest.php diff --git a/src/Exception/DomainException.php b/src/Exception/DomainException.php new file mode 100644 index 0000000..a7d1ae2 --- /dev/null +++ b/src/Exception/DomainException.php @@ -0,0 +1,13 @@ +success = true; + $result->matchedParams = $matchedParams; + $result->matchedRouteName = $routeName; + $result->pathOffset = $pathOffset; + $result->matchedPathLength = $matchedPathLength; + return $result; + } + + /** + * Create failed routing result + */ + public static function fromRouteFailure() : PartialRouteResult + { + $result = new self(); + $result->success = false; + return $result; + } + + /** + * Create routing failure result where http method is not allowed for the + * otherwise routable request + */ + public static function fromMethodFailure( + array $allowedMethods, + int $pathOffset, + int $matchedPathLength + ) : PartialRouteResult { + if (empty($allowedMethods)) { + throw new DomainException('Method failure requires list of allowed methods'); + } + if ($pathOffset < 0) { + throw new DomainException('Path offset cannot be negative'); + } + if ($matchedPathLength < 0) { + throw new DomainException('Matched path length cannot be negative'); + } + + $result = new self(); + $result->success = false; + $result->setAllowedMethods($allowedMethods); + $result->pathOffset = $pathOffset; + $result->matchedPathLength = $matchedPathLength; + return $result; + } + + /** + * Is this a routing success result? + */ + public function isSuccess() : bool + { + return $this->success; + } + + /** + * Is this a routing failure result? + */ + public function isFailure() : bool + { + return ! $this->success; + } + + /** + * Is this a result for failed routing due to HTTP method? + */ + public function isMethodFailure() : bool + { + if ($this->isSuccess() || empty($this->allowedMethods)) { + return false; + } + return true; + } + + /** + * Checks if partial route result is a full match for the provided uri path. + * Expects same uri as used for matching. + */ + public function isFullPathMatch(UriInterface $uri) : bool + { + // non http method failure is no match. For edge case of empty uri path + if ($this->isFailure() && ! $this->isMethodFailure()) { + return false; + } + $pathLength = strlen($uri->getPath()); + return $pathLength === ($this->pathOffset + $this->matchedPathLength); + } + + /** + * Produce a new partial route result with provided route name. Can only be used + * with successful result. + * + * @param string $flag Signifies mode of setting route name: + * - {@see RouteResult::NAME_REPLACE} replaces existing route name + * - {@see RouteResult::NAME_PREPEND} prepends as a parent route part name. + * - {@see RouteResult::NAME_APPEND} appends as a child route part name. + */ + public function withMatchedRouteName(string $routeName, $flag = RouteResult::NAME_REPLACE) : PartialRouteResult + { + if (empty($routeName)) { + throw new DomainException('Route name cannot be empty'); + } + if (! $this->isSuccess()) { + throw new RuntimeException('Only successful routing can have matched route name'); + } + $result = clone $this; + + // If no matched route name is set, simply replace value + if ($flag === RouteResult::NAME_REPLACE || $this->matchedRouteName === null) { + $result->matchedRouteName = $routeName; + return $result; + } + + if ($flag === RouteResult::NAME_PREPEND) { + $routeName = sprintf('%s/%s', $routeName, $this->matchedRouteName); + } elseif ($flag === RouteResult::NAME_APPEND) { + $routeName = sprintf('%s/%s', $this->matchedRouteName, $routeName); + } else { + throw new DomainException('Unknown flag for setting matched route name'); + } + $result->matchedRouteName = $routeName; + + return $result; + } + + /** + * Produce a new partial route result with provided matched parameters. Can only be + * used with successful result. + */ + public function withMatchedParams(array $params) : PartialRouteResult + { + if (! $this->isSuccess()) { + throw new RuntimeException('Only successful routing can have matched params'); + } + $result = clone $this; + $result->matchedParams = $params; + return $result; + } + + /** + * Matched route name on successful routing. + * Can be null. Route name is normally set by the route stack and can differ + * for same route instance if it is used in several places. + */ + public function getMatchedRouteName() : ?string + { + return $this->matchedRouteName; + } + + /** + * Matched parameters on successful routing + */ + public function getMatchedParams() : array + { + return $this->matchedParams; + } + + /** + * Returns list of allowed methods on method failure. + */ + public function getAllowedMethods() : array + { + return $this->allowedMethods; + } + + /** + * Offset used for partial routing matching + */ + public function getUsedPathOffset() : int + { + return $this->pathOffset; + } + + /** + * Matched uri path length, starting from offset + */ + public function getMatchedPathLength() : int + { + return $this->matchedPathLength; + } + + /** + * Helper function to deduplicate and normalize HTTP method names + */ + private function setAllowedMethods(array $methods) : void + { + $methods = array_keys(array_change_key_case( + array_flip($methods), + CASE_UPPER + )); + $this->allowedMethods = $methods; + } + + /** + * Disallow new-ing route result + */ + private function __construct() + { + } +} diff --git a/src/RouteMatch.php b/src/RouteMatch.php index 439aa14..b2693c7 100644 --- a/src/RouteMatch.php +++ b/src/RouteMatch.php @@ -9,8 +9,14 @@ namespace Zend\Router; +use Zend\Router\Exception\RuntimeException; + +use function array_key_exists; + /** * RouteInterface match. + * + * @deprecated */ class RouteMatch { @@ -30,19 +36,28 @@ class RouteMatch /** * Create a RouteMatch with given parameters. - * - * @param array $params */ public function __construct(array $params) { $this->params = $params; } + public static function fromRouteResult(RouteResult $result) : self + { + if (! $result->isSuccess()) { + throw new RuntimeException('Route match cannot be created from failure route result'); + } + $match = new static($result->getMatchedParams()); + $match->setMatchedRouteName($result->getMatchedRouteName()); + + return $match; + } + /** * Set name of matched route. * - * @param string $name - * @return RouteMatch + * @param string $name + * @return $this */ public function setMatchedRouteName($name) { @@ -63,9 +78,9 @@ public function getMatchedRouteName() /** * Set a parameter. * - * @param string $name - * @param mixed $value - * @return RouteMatch + * @param string $name + * @param mixed $value + * @return $this */ public function setParam($name, $value) { @@ -86,8 +101,8 @@ public function getParams() /** * Get a specific parameter. * - * @param string $name - * @param mixed $default + * @param string $name + * @param null|mixed $default * @return mixed */ public function getParam($name, $default = null) diff --git a/src/RouteResult.php b/src/RouteResult.php new file mode 100644 index 0000000..c85f2cf --- /dev/null +++ b/src/RouteResult.php @@ -0,0 +1,211 @@ +success = true; + $result->matchedParams = $matchedParams; + $result->matchedRouteName = $routeName; + return $result; + } + + /** + * Create failed routing result + */ + public static function fromRouteFailure() : self + { + $result = new self(); + $result->success = false; + return $result; + } + + /** + * Create routing failure result where http method is not allowed for the + * otherwise routable request + */ + public static function fromMethodFailure(array $allowedMethods) : self + { + if (empty($allowedMethods)) { + throw new DomainException('Method failure requires list of allowed methods'); + } + $result = new self(); + $result->success = false; + $result->setAllowedMethods($allowedMethods); + return $result; + } + + /** + * Is this a routing success result? + */ + public function isSuccess() : bool + { + return $this->success; + } + + /** + * Is this a routing failure result? + */ + public function isFailure() : bool + { + return ! $this->success; + } + + /** + * Is this a result for failed routing due to HTTP method? + */ + public function isMethodFailure() : bool + { + if ($this->isSuccess() || empty($this->allowedMethods)) { + return false; + } + return true; + } + + /** + * Produce a new route result with provided route name. Can only be used + * with successful result. + * + * @param string $flag Signifies mode of setting route name: + * - {@see RouteResult::NAME_REPLACE} replaces existing route name + * - {@see RouteResult::NAME_PREPEND} prepends as a parent route part name. + * - {@see RouteResult::NAME_APPEND} appends as a child route part name. + */ + public function withMatchedRouteName(string $routeName, $flag = self::NAME_REPLACE) : self + { + if (empty($routeName)) { + throw new DomainException('Route name cannot be empty'); + } + if (! $this->isSuccess()) { + throw new RuntimeException('Only successful routing can have matched route name'); + } + $result = clone $this; + + // If no matched route name is set, simply replace value + if ($flag === self::NAME_REPLACE || $this->matchedRouteName === null) { + $result->matchedRouteName = $routeName; + return $result; + } + + if ($flag === self::NAME_PREPEND) { + $routeName = sprintf('%s/%s', $routeName, $this->matchedRouteName); + } elseif ($flag === self::NAME_APPEND) { + $routeName = sprintf('%s/%s', $this->matchedRouteName, $routeName); + } else { + throw new DomainException('Unknown flag for setting matched route name'); + } + $result->matchedRouteName = $routeName; + + return $result; + } + + /** + * Produce a new route result with provided matched parameters. Can only be + * used with successful result. + */ + public function withMatchedParams(array $params) : self + { + if (! $this->isSuccess()) { + throw new RuntimeException('Only successful routing can have matched params'); + } + $result = clone $this; + $result->matchedParams = $params; + return $result; + } + + /** + * Matched route name on successful routing. + * Can be null. Route name is normally set by the route stack and can differ + * for same route instance if it is used in several places. + */ + public function getMatchedRouteName() : ?string + { + return $this->matchedRouteName; + } + + /** + * Matched parameters on successful routing + */ + public function getMatchedParams() : array + { + return $this->matchedParams; + } + + /** + * Returns list of allowed methods on method failure. + */ + public function getAllowedMethods() : array + { + return $this->allowedMethods; + } + + /** + * Helper function to deduplicate and normalize HTTP method names + */ + private function setAllowedMethods(array $methods) : void + { + $methods = array_keys(array_change_key_case( + array_flip($methods), + CASE_UPPER + )); + $this->allowedMethods = $methods; + } + + /** + * Disallow new-ing route result + */ + private function __construct() + { + } +} diff --git a/test/PartialRouteResultTest.php b/test/PartialRouteResultTest.php new file mode 100644 index 0000000..cf51136 --- /dev/null +++ b/test/PartialRouteResultTest.php @@ -0,0 +1,267 @@ +assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); + $this->assertFalse($result->isSuccess()); + } + + public function testFromMethodFailure() + { + $methods = ['GET', 'POST']; + $result = PartialRouteResult::fromMethodFailure($methods, 10, 20); + $this->assertTrue($result->isFailure()); + $this->assertTrue($result->isMethodFailure()); + $this->assertFalse($result->isSuccess()); + $this->assertEquals($methods, $result->getAllowedMethods()); + $this->assertEquals(10, $result->getUsedPathOffset()); + $this->assertEquals(20, $result->getMatchedPathLength()); + } + + public function testFromMethodFailureDeduplicatesAndNormalizesHttpMethods() + { + $methods = ['GeT', 'get', 'POST', 'POST']; + $result = PartialRouteResult::fromMethodFailure($methods, 0, 0); + $this->assertEquals(['GET', 'POST'], $result->getAllowedMethods()); + } + + public function testFromMethodFailureRejectsNegativeOffset() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $result = PartialRouteResult::fromMethodFailure(['GET'], -1, 0); + } + + public function testFromMethodFailureRejectsNegativeMatchedLength() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Matched path length cannot be negative'); + PartialRouteResult::fromMethodFailure(['GET'], 0, -1); + } + + /** + * Empty list can occur on allowed methods intersect in Part route. Eg when + * parent route allows only GET and child only POST. Route must handle + * such occurrence. + */ + public function testFromMethodFailureThrowsOnEmptyAllowedMethodsList() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Method failure requires list of allowed methods'); + PartialRouteResult::fromMethodFailure([], 10, 20); + } + + public function testFromRouteMatchIsSuccessful() + { + $result = PartialRouteResult::fromRouteMatch([], 0, 0); + $this->assertFalse($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); + $this->assertTrue($result->isSuccess()); + } + + public function testFromRouteMatchSetsPathOffsetAndMatchedLength() + { + $result = PartialRouteResult::fromRouteMatch([], 10, 5); + $this->assertEquals(10, $result->getUsedPathOffset()); + $this->assertEquals(5, $result->getMatchedPathLength()); + } + + public function testFromRouteMatchWithNoRouteNameProvided() + { + $result = PartialRouteResult::fromRouteMatch([], 0, 0); + $this->assertNull($result->getMatchedRouteName()); + } + + public function testFromRouteMatchSetsMatchedRouteNameWhenProvided() + { + $result = PartialRouteResult::fromRouteMatch([], 0, 0, 'bar'); + $this->assertEquals('bar', $result->getMatchedRouteName()); + } + + public function testFromRouteMatchSetsMatchedParameters() + { + $params = ['foo' => 'bar']; + $result = PartialRouteResult::fromRouteMatch($params, 0, 0); + $this->assertEquals($params, $result->getMatchedParams()); + } + + public function testFromRouteMatchRejectsNegativeOffset() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + PartialRouteResult::fromRouteMatch([], -1, 0); + } + + public function testFromRouteMatchRejectsNegativeMatchedLength() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Matched path length cannot be negative'); + PartialRouteResult::fromRouteMatch([], 0, -1); + } + + public function testWithRouteNameReplacesNameInNewInstance() + { + $result1 = PartialRouteResult::fromRouteMatch([], 0, 0, 'foo'); + $result2 = $result1->withMatchedRouteName('bar'); + $this->assertNotSame($result1, $result2); + $this->assertSame('foo', $result1->getMatchedRouteName()); + $this->assertSame('bar', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameRetainsPathOffsetAndMatchedLength() + { + $result1 = PartialRouteResult::fromRouteMatch([], 10, 5, 'foo'); + $result2 = $result1->withMatchedRouteName('bar'); + $this->assertEquals(10, $result2->getUsedPathOffset()); + $this->assertEquals(5, $result2->getMatchedPathLength()); + } + + public function testWithRouteNameWithPrependFlagPrependsNameToExisting() + { + $result1 = PartialRouteResult::fromRouteMatch([], 0, 0, 'foo'); + $result2 = $result1->withMatchedRouteName('bar', RouteResult::NAME_PREPEND); + $this->assertNotSame($result1, $result2); + $this->assertSame('foo', $result1->getMatchedRouteName()); + $this->assertSame('bar/foo', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameWithPrependFlagSetsNameWhenRouteNameIsNotSet() + { + $result1 = PartialRouteResult::fromRouteMatch([], 0, 0, null); + $result2 = $result1->withMatchedRouteName('bar', RouteResult::NAME_PREPEND); + $this->assertSame('bar', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameWithAppendFlagAppendsNameToExisting() + { + $result1 = PartialRouteResult::fromRouteMatch([], 0, 0, 'foo'); + $result2 = $result1->withMatchedRouteName('bar', RouteResult::NAME_APPEND); + $this->assertNotSame($result1, $result2); + $this->assertSame('foo', $result1->getMatchedRouteName()); + $this->assertSame('foo/bar', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameWithAppendFlagSetsNameWhenRouteNameIsNotSet() + { + $result1 = PartialRouteResult::fromRouteMatch([], 0, 0, null); + $result2 = $result1->withMatchedRouteName('bar', RouteResult::NAME_APPEND); + $this->assertSame('bar', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameThrowsForUnsuccessfulResult() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Only successful routing can have matched route name'); + $result = PartialRouteResult::fromRouteFailure(); + $result->withMatchedRouteName('foo'); + } + + public function testWithRouteNameRejectsEmptyName() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Route name cannot be empty'); + $result = PartialRouteResult::fromRouteMatch([], 0, 0, 'foo'); + $result->withMatchedRouteName(''); + } + + public function testWithRouteNameThrowsOnUnknownFlag() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Unknown flag'); + $result = PartialRouteResult::fromRouteMatch([], 0, 0, 'foo'); + $result->withMatchedRouteName('bar', 'unknown'); + } + + public function testWithMatchedParamsReplacesInNewInstance() + { + $params1 = ['foo' => 'bar']; + $params2 = ['baz' => 'qux']; + $result1 = PartialRouteResult::fromRouteMatch($params1, 0, 0, null); + $result2 = $result1->withMatchedParams($params2); + $this->assertNotSame($result1, $result2); + $this->assertSame($params1, $result1->getMatchedParams()); + $this->assertSame($params2, $result2->getMatchedParams()); + } + + public function testWithMatchedParamsThrowsForUnsuccessfulResult() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Only successful routing can have matched params'); + $result = PartialRouteResult::fromRouteFailure(); + $result->withMatchedParams(['foo' => 'bar']); + } + + public function provideFullPathMatchData() + { + return [ + 'full match' => [ + new Uri('/foo'), + 0, + 4, + true + ], + 'partial match' => [ + new Uri('/foo'), + 0, + 3, + false + ], + 'offset full match' => [ + new Uri('/foo'), + 1, + 3, + true + ], + 'offset partial match' => [ + new Uri('/foo/bar'), + 1, + 3, + false + ], + 'empty uri path' => [ + new Uri(''), + 0, + 0, + true + ], + ]; + } + + /** + * @dataProvider provideFullPathMatchData + */ + public function testIsFullPathMatch(Uri $uri, int $offset, int $length, bool $fullMatch) + { + $result = PartialRouteResult::fromRouteMatch([], $offset, $length); + $this->assertEquals($fullMatch, $result->isFullPathMatch($uri)); + } + + public function testIsNeverAFullPathMatchOnRouteFailure() + { + $uri = new Uri(''); + $result = PartialRouteResult::fromRouteFailure(); + $this->assertFalse($result->isFullPathMatch($uri)); + } +} diff --git a/test/RouteMatchTest.php b/test/RouteMatchTest.php index 99f0233..1888acb 100644 --- a/test/RouteMatchTest.php +++ b/test/RouteMatchTest.php @@ -10,8 +10,13 @@ namespace ZendTest\Router; use PHPUnit\Framework\TestCase; +use Zend\Router\Exception\RuntimeException; use Zend\Router\RouteMatch; +use Zend\Router\RouteResult; +/** + * @covers \Zend\Router\RouteMatch + */ class RouteMatchTest extends TestCase { public function testParamsAreStored() @@ -57,4 +62,20 @@ public function testGetNonExistentParamWithDefault() $this->assertEquals('bar', $match->getParam('foo', 'bar')); } + + public function testCreateFromRouteResult() + { + $routeResult = RouteResult::fromRouteMatch(['foo' => 'bar'], 'baz'); + $match = RouteMatch::fromRouteResult($routeResult); + $this->assertEquals('bar', $match->getParam('foo')); + $this->assertEquals('baz', $match->getMatchedRouteName()); + } + + public function testCantCreateFromFailureRouteResult() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Route match cannot be created from failure route result'); + $routeResult = RouteResult::fromRouteFailure(); + RouteMatch::fromRouteResult($routeResult); + } } diff --git a/test/RouteResultTest.php b/test/RouteResultTest.php new file mode 100644 index 0000000..7c28bee --- /dev/null +++ b/test/RouteResultTest.php @@ -0,0 +1,169 @@ +assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); + $this->assertFalse($result->isSuccess()); + } + + public function testFromMethodFailure() + { + $methods = ['GET', 'POST']; + $result = RouteResult::fromMethodFailure($methods); + $this->assertTrue($result->isFailure()); + $this->assertTrue($result->isMethodFailure()); + $this->assertFalse($result->isSuccess()); + $this->assertEquals($methods, $result->getAllowedMethods()); + } + + public function testFromMethodFailureDeduplicatesAndNormalizesHttpMethods() + { + $methods = ['GeT', 'get', 'POST', 'POST']; + $result = RouteResult::fromMethodFailure($methods); + $this->assertEquals(['GET', 'POST'], $result->getAllowedMethods()); + } + + /** + * Empty list can occur on allowed methods intersect in Part route. Eg when + * parent route allows only GET and child only POST. Route must handle + * such occurrence. + */ + public function testFromMethodFailureThrowsOnEmptyAllowedMethodsList() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Method failure requires list of allowed methods'); + RouteResult::fromMethodFailure([]); + } + + public function testFromRouteMatchIsSuccessful() + { + $result = RouteResult::fromRouteMatch([], null); + $this->assertFalse($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); + $this->assertTrue($result->isSuccess()); + } + + public function testFromRouteMatchWithNoRouteNameProvided() + { + $result = RouteResult::fromRouteMatch([]); + $this->assertNull($result->getMatchedRouteName()); + } + + public function testFromRouteMatchSetsMatchedRouteNameWhenProvided() + { + $result = RouteResult::fromRouteMatch([], 'bar'); + $this->assertEquals('bar', $result->getMatchedRouteName()); + } + + public function testFromRouteMatchSetsMatchedParameters() + { + $params = ['foo' => 'bar']; + $result = RouteResult::fromRouteMatch($params); + $this->assertEquals($params, $result->getMatchedParams()); + } + + public function testWithRouteNameReplacesNameInNewInstance() + { + $result1 = RouteResult::fromRouteMatch([], 'foo'); + $result2 = $result1->withMatchedRouteName('bar'); + $this->assertNotSame($result1, $result2); + $this->assertSame('foo', $result1->getMatchedRouteName()); + $this->assertSame('bar', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameWithPrependFlagPrependsNameToExisting() + { + $result1 = RouteResult::fromRouteMatch([], 'foo'); + $result2 = $result1->withMatchedRouteName('bar', RouteResult::NAME_PREPEND); + $this->assertNotSame($result1, $result2); + $this->assertSame('foo', $result1->getMatchedRouteName()); + $this->assertSame('bar/foo', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameWithPrependFlagSetsNameWhenRouteNameIsNotSet() + { + $result1 = RouteResult::fromRouteMatch([], null); + $result2 = $result1->withMatchedRouteName('bar', RouteResult::NAME_PREPEND); + $this->assertSame('bar', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameWithAppendFlagAppendsNameToExisting() + { + $result1 = RouteResult::fromRouteMatch([], 'foo'); + $result2 = $result1->withMatchedRouteName('bar', RouteResult::NAME_APPEND); + $this->assertNotSame($result1, $result2); + $this->assertSame('foo', $result1->getMatchedRouteName()); + $this->assertSame('foo/bar', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameWithAppendFlagSetsNameWhenRouteNameIsNotSet() + { + $result1 = RouteResult::fromRouteMatch([], null); + $result2 = $result1->withMatchedRouteName('bar', RouteResult::NAME_APPEND); + $this->assertSame('bar', $result2->getMatchedRouteName()); + } + + public function testWithRouteNameThrowsForUnsuccessfulResult() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Only successful routing can have matched route name'); + $result = RouteResult::fromRouteFailure(); + $result->withMatchedRouteName('foo'); + } + + public function testWithRouteNameRejectsEmptyName() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Route name cannot be empty'); + $result = RouteResult::fromRouteMatch([], 'foo'); + $result->withMatchedRouteName(''); + } + + public function testWithRouteNameThrowsOnUnknownFlag() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Unknown flag'); + $result = RouteResult::fromRouteMatch([], 'foo'); + $result->withMatchedRouteName('bar', 'unknown'); + } + + public function testWithMatchedParamsReplacesInNewInstance() + { + $params1 = ['foo' => 'bar']; + $params2 = ['baz' => 'qux']; + $result1 = RouteResult::fromRouteMatch($params1, null); + $result2 = $result1->withMatchedParams($params2); + $this->assertNotSame($result1, $result2); + $this->assertSame($params1, $result1->getMatchedParams()); + $this->assertSame($params2, $result2->getMatchedParams()); + } + + public function testWithMatchedParamsThrowsForUnsuccessfulResult() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Only successful routing can have matched params'); + $result = RouteResult::fromRouteFailure(); + $result->withMatchedParams(['foo' => 'bar']); + } +} From 627a9d9e80a08c48fbfdedf4bfc0183a84499c98 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 3 Mar 2018 19:14:30 +1000 Subject: [PATCH 12/47] Change RouteInterface to PSR-7 - Change RouteInterface::match() signature, it now requires PSR-7 ServerRequestInterface and always returns RouteResult - Change RouteInterface::assemble() signature, it now requires PSR-7 UriInterface as first parameter and returns UriInterface. Usage of UriInterface allows for more flexibility in assembling uri from tree route parts as compared to simple string manipulation. - Drop factory() from RouteInterface. Routes can be greatly simplified by moving the creation logic out. - Introduce PartialRouteInterface extending RouteInterface to allow partial matching enforced by contract. - Drop Http specific RouteInterface as zend-router is now Http only - RouteStackInterface is extended with additional getters previously present only in SimpleRouteStack and by extension in TreeRouteStack. Fluent interface is removed. --- src/Http/RouteInterface.php | 25 ----------------------- src/PartialRouteInterface.php | 28 ++++++++++++++++++++++++++ src/RouteInterface.php | 29 ++++----------------------- src/RouteStackInterface.php | 37 ++++++++++++++++++----------------- 4 files changed, 51 insertions(+), 68 deletions(-) delete mode 100644 src/Http/RouteInterface.php create mode 100644 src/PartialRouteInterface.php diff --git a/src/Http/RouteInterface.php b/src/Http/RouteInterface.php deleted file mode 100644 index 18a833d..0000000 --- a/src/Http/RouteInterface.php +++ /dev/null @@ -1,25 +0,0 @@ - Date: Sun, 4 Mar 2018 15:07:03 +1000 Subject: [PATCH 13/47] Route stack refactor stub --- src/SimpleRouteStack.php | 283 +++------------ src/TreeRouteStack.php | 472 ++----------------------- test/SimpleRouteStackTest.php | 200 +++-------- test/TestAsset/DummyRoute.php | 39 +- test/TestAsset/DummyRouteWithParam.php | 33 +- test/TreeRouteStackTest.php | 446 ++++++----------------- 6 files changed, 239 insertions(+), 1234 deletions(-) diff --git a/src/SimpleRouteStack.php b/src/SimpleRouteStack.php index 3794ef0..e2b98d2 100644 --- a/src/SimpleRouteStack.php +++ b/src/SimpleRouteStack.php @@ -9,10 +9,14 @@ namespace Zend\Router; -use Traversable; -use Zend\ServiceManager\ServiceManager; -use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\Exception\RuntimeException; + +use function array_merge; +use function array_reduce; +use function sprintf; /** * Simple route stack implementation. @@ -26,13 +30,6 @@ class SimpleRouteStack implements RouteStackInterface */ protected $routes; - /** - * Route plugin manager - * - * @var RoutePluginManager - */ - protected $routePluginManager; - /** * Default parameters. * @@ -42,302 +39,114 @@ class SimpleRouteStack implements RouteStackInterface /** * Create a new simple route stack. - * - * @param RoutePluginManager $routePluginManager */ - public function __construct(RoutePluginManager $routePluginManager = null) + public function __construct() { $this->routes = new PriorityList(); - - if (null === $routePluginManager) { - $routePluginManager = new RoutePluginManager(new ServiceManager()); - } - - $this->routePluginManager = $routePluginManager; - - $this->init(); } - /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $options - * @return SimpleRouteStack - * @throws Exception\InvalidArgumentException - */ - public static function factory($options = []) + public function addRoutes(iterable $routes) : void { - 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__ - )); - } - - $routePluginManager = null; - if (isset($options['route_plugins'])) { - $routePluginManager = $options['route_plugins']; - } - - $instance = new static($routePluginManager); - - if (isset($options['routes'])) { - $instance->addRoutes($options['routes']); - } - - if (isset($options['default_params'])) { - $instance->setDefaultParams($options['default_params']); - } - - return $instance; - } - - /** - * Init method for extending classes. - * - * @return void - */ - protected function init() - { - } - - /** - * Set the route plugin manager. - * - * @param RoutePluginManager $routePlugins - * @return SimpleRouteStack - */ - public function setRoutePluginManager(RoutePluginManager $routePlugins) - { - $this->routePluginManager = $routePlugins; - return $this; - } - - /** - * Get the route plugin manager. - * - * @return RoutePluginManager - */ - public function getRoutePluginManager() - { - return $this->routePluginManager; - } - - /** - * addRoutes(): defined by RouteStackInterface interface. - * - * @see RouteStackInterface::addRoutes() - * @param array|Traversable $routes - * @return SimpleRouteStack - * @throws Exception\InvalidArgumentException - */ - 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; } - /** - * addRoute(): defined by RouteStackInterface interface. - * - * @see RouteStackInterface::addRoute() - * @param string $name - * @param mixed $route - * @param int $priority - * @return SimpleRouteStack - */ - public function addRoute($name, $route, $priority = null) + public function addRoute(string $name, RouteInterface $route, int $priority = null) : void { - 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; } - /** - * removeRoute(): defined by RouteStackInterface interface. - * - * @see RouteStackInterface::removeRoute() - * @param string $name - * @return SimpleRouteStack - */ - public function removeRoute($name) + public function removeRoute(string $name) : void { $this->routes->remove($name); - return $this; } - /** - * setRoutes(): defined by RouteStackInterface interface. - * - * @param array|Traversable $routes - * @return SimpleRouteStack - */ - public function setRoutes($routes) + public function setRoutes(iterable $routes) : void { $this->routes->clear(); $this->addRoutes($routes); - return $this; } - /** - * Get the added routes - * - * @return Traversable list of all routes - */ - public function getRoutes() + public function getRoutes() : array { - return $this->routes; + return $this->routes->toArray($this->routes::EXTR_DATA); } - /** - * Check if a route with a specific name exists - * - * @param string $name - * @return bool true if route exists - */ - public function hasRoute($name) + public function hasRoute(string $name) : bool { return $this->routes->get($name) !== null; } - /** - * Get a route by name - * - * @param string $name - * @return RouteInterface the route - */ - public function getRoute($name) + public function getRoute(string $name) : ?RouteInterface { return $this->routes->get($name); } - /** - * Set a default parameters. - * - * @param array $params - * @return SimpleRouteStack - */ - public function setDefaultParams(array $params) + public function setDefaultParams(array $params) : void { $this->defaultParams = $params; - return $this; } /** * Set a default parameter. * - * @param string $name - * @param mixed $value - * @return SimpleRouteStack + * @param mixed $value */ - public function setDefaultParam($name, $value) + public function setDefaultParam(string $name, $value) : void { $this->defaultParams[$name] = $value; - return $this; } - /** - * Create a route from array specifications. - * - * @param array|Traversable $specs - * @return RouteInterface - * @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 \Zend\Router\RouteInterface::match() - * @param Request $request - * @return RouteMatch|null - */ - public function match(Request $request) + public function match(Request $request, int $pathOffset = 0, array $options = []) : RouteResult { + $methodFailureResults = []; foreach ($this->routes as $name => $route) { - if (($match = $route->match($request)) instanceof RouteMatch) { - $match->setMatchedRouteName($name); - - foreach ($this->defaultParams as $paramName => $value) { - if ($match->getParam($paramName) === null) { - $match->setParam($paramName, $value); - } - } - - return $match; + /** @var RouteInterface $route */ + $result = $route->match($request, $pathOffset, $options); + if ($result->isSuccess()) { + $result = $result->withMatchedRouteName($name); + $result = $result->withMatchedParams( + array_merge($this->defaultParams, $result->getMatchedParams()) + ); + return $result; + } + if ($result->isMethodFailure()) { + $methodFailureResults[] = $result; } } - return; + if (! empty($methodFailureResults)) { + $allowedMethods = array_reduce($methodFailureResults, function (array $methods, RouteResult $result) { + return $methods + $result->getAllowedMethods(); + }, []); + return RouteResult::fromMethodFailure($allowedMethods); + } + return RouteResult::fromRouteFailure(); } /** - * assemble(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - * @throws Exception\InvalidArgumentException - * @throws Exception\RuntimeException + * @throws InvalidArgumentException + * @throws RuntimeException */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { if (! isset($options['name'])) { - throw new Exception\InvalidArgumentException('Missing "name" option'); + throw new InvalidArgumentException('Missing "name" option'); } $route = $this->routes->get($options['name']); if (! $route) { - throw new Exception\RuntimeException(sprintf('Route with name "%s" not found', $options['name'])); + throw new RuntimeException(sprintf('Route with name "%s" not found', $options['name'])); } unset($options['name']); - return $route->assemble(array_merge($this->defaultParams, $params), $options); + return $route->assemble($uri, array_merge($this->defaultParams, $params), $options); } } diff --git a/src/TreeRouteStack.php b/src/TreeRouteStack.php index 7ef3e5f..90b8346 100644 --- a/src/TreeRouteStack.php +++ b/src/TreeRouteStack.php @@ -9,360 +9,66 @@ namespace Zend\Router; -use ArrayObject; -use Traversable; -use Zend\Router\Exception; -use Zend\Router\Http\Chain; -use Zend\Router\Http\Hostname; -use Zend\Router\Http\Literal; -use Zend\Router\Http\Method; -use Zend\Router\Http\Part; -use Zend\Router\Http\Regex; -use Zend\Router\Http\RouteInterface; -use Zend\Router\Http\RouteMatch; -use Zend\Router\Http\Scheme; -use Zend\Router\Http\Segment; -use Zend\Router\RouteInvokableFactory; -use Zend\Router\SimpleRouteStack; -use Zend\ServiceManager\Config; -use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; -use Zend\Uri\Http as HttpUri; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\Exception\RuntimeException; +use Zend\Router\Route\Method; + +use function array_merge; +use function explode; +use function sprintf; /** * Tree search implementation. */ class TreeRouteStack extends SimpleRouteStack { - /** - * Base URL. - * - * @var string - */ - protected $baseUrl; - - /** - * Request URI. - * - * @var HttpUri - */ - protected $requestUri; - - /** - * Prototype routes. - * - * We use an ArrayObject in this case so we can easily pass it down the tree - * by reference. - * - * @var ArrayObject - */ - protected $prototypes; - - /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $options - * @return SimpleRouteStack - * @throws Exception\InvalidArgumentException - */ - public static function factory($options = []) - { - 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__ - )); - } - - $instance = parent::factory($options); - - if (isset($options['prototypes'])) { - $instance->addPrototypes($options['prototypes']); - } - - return $instance; - } - - /** - * init(): defined by SimpleRouteStack. - * - * @see SimpleRouteStack::init() - */ - protected function init() + public function match(Request $request, int $pathOffset = 0, array $options = []) : RouteResult { - $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, - - // v2 normalized names - - 'zendmvcrouterhttpchain' => RouteInvokableFactory::class, - 'zendmvcrouterhttphostname' => RouteInvokableFactory::class, - 'zendmvcrouterhttpliteral' => RouteInvokableFactory::class, - 'zendmvcrouterhttpmethod' => RouteInvokableFactory::class, - 'zendmvcrouterhttppart' => RouteInvokableFactory::class, - 'zendmvcrouterhttpregex' => RouteInvokableFactory::class, - 'zendmvcrouterhttpscheme' => RouteInvokableFactory::class, - 'zendmvcrouterhttpsegment' => RouteInvokableFactory::class, - ], - ]))->configureServiceManager($this->routePluginManager); - } - - /** - * addRoute(): defined by RouteStackInterface interface. - * - * @see RouteStackInterface::addRoute() - * @param string $name - * @param mixed $route - * @param int $priority - * @return TreeRouteStack - */ - public function addRoute($name, $route, $priority = null) - { - if (! $route instanceof RouteInterface) { - $route = $this->routeFromArray($route); - } - - return parent::addRoute($name, $route, $priority); - } - - /** - * routeFromArray(): defined by SimpleRouteStack. - * - * @see SimpleRouteStack::routeFromArray() - * @param string|array|Traversable $specs - * @return RouteInterface - * @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 - */ - protected function routeFromArray($specs) - { - if (is_string($specs)) { - if (null === ($route = $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 (isset($specs['chain_routes'])) { - if (! is_array($specs['chain_routes'])) { - throw new Exception\InvalidArgumentException('Chain routes must be an array or Traversable object'); + $allowedMethods = []; + foreach ($this->routes as $name => $route) { + /** @var RouteInterface $route */ + $result = $route->match($request, $pathOffset, $options); + if ($result->isSuccess()) { + $result = $result->withMatchedRouteName($name, RouteResult::NAME_PREPEND); + $result = $result->withMatchedParams( + array_merge($this->defaultParams, $result->getMatchedParams()) + ); + return $result; } - - $chainRoutes = array_merge([$specs], $specs['chain_routes']); - unset($chainRoutes[0]['chain_routes']); - - if (isset($specs['child_routes'])) { - unset($chainRoutes[0]['child_routes']); + if ($result->isMethodFailure()) { + $options[Method::OPTION_FORCE_METHOD_FAILURE] = true; + $allowedMethods = array_merge($allowedMethods, $result->getAllowedMethods()); } - - $options = [ - 'routes' => $chainRoutes, - 'route_plugins' => $this->routePluginManager, - 'prototypes' => $this->prototypes, - ]; - - $route = $this->routePluginManager->get('chain', $options); - } else { - $route = parent::routeFromArray($specs); - } - - if (! $route instanceof RouteInterface) { - throw new Exception\RuntimeException('Given route does not implement HTTP route interface'); } - if (isset($specs['child_routes'])) { - $options = [ - 'route' => $route, - 'may_terminate' => (isset($specs['may_terminate']) && $specs['may_terminate']), - 'child_routes' => $specs['child_routes'], - 'route_plugins' => $this->routePluginManager, - 'prototypes' => $this->prototypes, - ]; - - $priority = (isset($route->priority) ? $route->priority : null); - - $route = $this->routePluginManager->get('part', $options); - $route->priority = $priority; + if (! empty($allowedMethods)) { + return RouteResult::fromMethodFailure($allowedMethods); } - - return $route; + return RouteResult::fromRouteFailure(); } /** - * Add multiple prototypes at once. - * - * @param Traversable $routes - * @return TreeRouteStack - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException + * @throws RuntimeException */ - public function addPrototypes($routes) - { - 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); - } - - return $this; - } - - /** - * Add a prototype. - * - * @param string $name - * @param mixed $route - * @return TreeRouteStack - */ - public function addPrototype($name, $route) - { - if (! $route instanceof RouteInterface) { - $route = $this->routeFromArray($route); - } - - $this->prototypes[$name] = $route; - - return $this; - } - - /** - * Get a prototype. - * - * @param string $name - * @return RouteInterface|null - */ - public function getPrototype($name) - { - if (isset($this->prototypes[$name])) { - return $this->prototypes[$name]; - } - - return; - } - - /** - * match(): defined by \Zend\Router\RouteInterface - * - * @see \Zend\Router\RouteInterface::match() - * @param Request $request - * @param integer|null $pathOffset - * @param array $options - * @return RouteMatch|null - */ - public function match(Request $request, $pathOffset = null, array $options = []) - { - if (! method_exists($request, 'getUri')) { - return; - } - - if ($this->baseUrl === null && method_exists($request, 'getBaseUrl')) { - $this->setBaseUrl($request->getBaseUrl()); - } - - $uri = $request->getUri(); - $baseUrlLength = strlen((string) $this->baseUrl) ?: null; - - if ($pathOffset !== null) { - $baseUrlLength += $pathOffset; - } - - if ($this->requestUri === null) { - $this->setRequestUri($uri); - } - - if ($baseUrlLength !== null) { - $pathLength = strlen((string) $uri->getPath()) - $baseUrlLength; - } else { - $pathLength = null; - } - - foreach ($this->routes as $name => $route) { - if (($match = $route->match($request, $baseUrlLength, $options)) instanceof RouteMatch - && ($pathLength === null || $match->getLength() === $pathLength) - ) { - $match->setMatchedRouteName($name); - - foreach ($this->defaultParams as $paramName => $value) { - if ($match->getParam($paramName) === null) { - $match->setParam($paramName, $value); - } - } - - return $match; - } - } - - return; - } - - /** - * assemble(): defined by \Zend\Router\RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - * @throws Exception\InvalidArgumentException - * @throws Exception\RuntimeException - */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { if (! isset($options['name'])) { - throw new Exception\InvalidArgumentException('Missing "name" option'); + throw new InvalidArgumentException('Missing "name" option'); } $names = explode('/', $options['name'], 2); $route = $this->routes->get($names[0]); if (! $route) { - throw new Exception\RuntimeException(sprintf('Route with name "%s" not found', $names[0])); + throw new RuntimeException(sprintf('Route with name "%s" not found', $names[0])); } if (isset($names[1])) { - if (! $route instanceof TreeRouteStack) { - throw new Exception\RuntimeException(sprintf( + if (! $route instanceof RouteStackInterface) { + throw new RuntimeException(sprintf( 'Route with name "%s" does not have child routes', $names[0] )); @@ -372,116 +78,6 @@ public function assemble(array $params = [], array $options = []) unset($options['name']); } - if (isset($options['only_return_path']) && $options['only_return_path']) { - return $this->baseUrl . $route->assemble(array_merge($this->defaultParams, $params), $options); - } - - if (! isset($options['uri'])) { - $uri = new HttpUri(); - - if (isset($options['force_canonical']) && $options['force_canonical']) { - if ($this->requestUri === null) { - throw new Exception\RuntimeException('Request URI has not been set'); - } - - $uri->setScheme($this->requestUri->getScheme()) - ->setHost($this->requestUri->getHost()) - ->setPort($this->requestUri->getPort()); - } - - $options['uri'] = $uri; - } else { - $uri = $options['uri']; - } - - $path = $this->baseUrl . $route->assemble(array_merge($this->defaultParams, $params), $options); - - if (isset($options['query'])) { - $uri->setQuery($options['query']); - } - - if (isset($options['fragment'])) { - $uri->setFragment($options['fragment']); - } - - if ((isset($options['force_canonical']) - && $options['force_canonical']) - || $uri->getHost() !== null - || $uri->getScheme() !== null - ) { - if (($uri->getHost() === null || $uri->getScheme() === null) && $this->requestUri === null) { - throw new Exception\RuntimeException('Request URI has not been set'); - } - - if ($uri->getHost() === null) { - $uri->setHost($this->requestUri->getHost()); - } - - if ($uri->getScheme() === null) { - $uri->setScheme($this->requestUri->getScheme()); - } - - $uri->setPath($path); - - if (! isset($options['normalize_path']) || $options['normalize_path']) { - $uri->normalize(); - } - - return $uri->toString(); - } elseif (! $uri->isAbsolute() && $uri->isValidRelative()) { - $uri->setPath($path); - - if (! isset($options['normalize_path']) || $options['normalize_path']) { - $uri->normalize(); - } - - return $uri->toString(); - } - - return $path; - } - - /** - * Set the base URL. - * - * @param string $baseUrl - * @return self - */ - public function setBaseUrl($baseUrl) - { - $this->baseUrl = rtrim($baseUrl, '/'); - return $this; - } - - /** - * Get the base URL. - * - * @return string - */ - public function getBaseUrl() - { - return $this->baseUrl; - } - - /** - * Set the request URI. - * - * @param HttpUri $uri - * @return TreeRouteStack - */ - public function setRequestUri(HttpUri $uri) - { - $this->requestUri = $uri; - return $this; - } - - /** - * Get the request URI. - * - * @return HttpUri - */ - public function getRequestUri() - { - return $this->requestUri; + return $route->assemble($uri, array_merge($this->defaultParams, $params), $options); } } diff --git a/test/SimpleRouteStackTest.php b/test/SimpleRouteStackTest.php index 54ad654..7144b82 100644 --- a/test/SimpleRouteStackTest.php +++ b/test/SimpleRouteStackTest.php @@ -1,7 +1,7 @@ setRoutePluginManager($routes); - - $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() + public function testAddRoutes() { $stack = new SimpleRouteStack(); $stack->addRoutes([ - 'foo' => new TestAsset\DummyRoute() + 'foo' => new TestAsset\DummyRoute(), ]); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertTrue($stack->match(new ServerRequest())->isSuccess()); } - public function testAddRoutesAsTraversable() - { - $stack = new SimpleRouteStack(); - $stack->addRoutes(new ArrayIterator([ - 'foo' => new TestAsset\DummyRoute() - ])); - - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); - } - - 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 testSetRoutes() { $stack = new SimpleRouteStack(); $stack->setRoutes([ - 'foo' => new TestAsset\DummyRoute() + 'foo' => new TestAsset\DummyRoute(), ]); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $this->assertTrue($stack->match(new ServerRequest())->isSuccess()); $stack->setRoutes([]); - $this->assertNull($stack->match(new Request())); - } - - public function testSetRoutesAsTraversable() - { - $stack = new SimpleRouteStack(); - $stack->setRoutes(new ArrayIterator([ - 'foo' => new TestAsset\DummyRoute() - ])); - - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); - - $stack->setRoutes(new ArrayIterator([])); - - $this->assertNull($stack->match(new Request())); + $this->assertFalse($stack->match(new ServerRequest())->isSuccess()); } - public function testremoveRouteAsArray() + public function testRemoveRoute() { $stack = new SimpleRouteStack(); $stack->addRoutes([ - 'foo' => new TestAsset\DummyRoute() + 'foo' => new TestAsset\DummyRoute(), ]); - $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'); - $stack->addRoute('foo', 'bar'); - } - - public function testAddRouteAsArrayWithoutOptions() - { - $stack = new SimpleRouteStack(); - $stack->addRoute('foo', [ - 'type' => TestAsset\DummyRoute::class - ]); - - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); - } - - public function testAddRouteAsArrayWithOptions() - { - $stack = new SimpleRouteStack(); - $stack->addRoute('foo', [ - 'type' => TestAsset\DummyRoute::class, - 'options' => [] - ]); - - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); - } - - public function testAddRouteAsArrayWithoutType() - { - $stack = new SimpleRouteStack(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Missing "type" option'); - $stack->addRoute('foo', []); - } - - public function testAddRouteAsArrayWithPriority() - { - $stack = new SimpleRouteStack(); - - $stack->addRoute('foo', [ - 'type' => TestAsset\DummyRouteWithParam::class, - 'priority' => 2 - ])->addRoute('bar', [ - 'type' => TestAsset\DummyRoute::class, - 'priority' => 1 - ]); - - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $stack->removeRoute('foo'); + $this->assertFalse($stack->match(new ServerRequest())->isSuccess()); } public function testAddRouteWithPriority() @@ -169,47 +64,39 @@ public function testAddRouteWithPriority() $route->priority = 2; $stack->addRoute('baz', $route); - $stack->addRoute('foo', [ - 'type' => TestAsset\DummyRoute::class, - 'priority' => 1 - ]); - - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); - } - - public function testAddRouteAsTraversable() - { - $stack = new SimpleRouteStack(); - $stack->addRoute('foo', new ArrayIterator([ - 'type' => TestAsset\DummyRoute::class - ])); + $stack->addRoute('foo', new TestAsset\DummyRoute(), 1); - $this->assertInstanceOf(RouteMatch::class, $stack->match(new Request())); + $result = $stack->match(new ServerRequest()); + $this->assertTrue($result->isSuccess()); + $this->assertArraySubset(['foo' => 'bar'], $result->getMatchedParams()); } public function testAssemble() { + $uri = new Uri(); $stack = new SimpleRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRoute()); - $this->assertEquals('', $stack->assemble([], ['name' => 'foo'])); + $this->assertEquals('', $stack->assemble($uri, [], ['name' => 'foo'])->getPath()); } public function testAssembleWithoutNameOption() { + $uri = new Uri(); $stack = new SimpleRouteStack(); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing "name" option'); - $stack->assemble(); + $stack->assemble($uri); } public function testAssembleNonExistentRoute() { + $uri = new Uri(); $stack = new SimpleRouteStack(); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Route with name "foo" not found'); - $stack->assemble([], ['name' => 'foo']); + $stack->assemble($uri, [], ['name' => 'foo']); } public function testDefaultParamIsAddedToMatch() @@ -218,7 +105,9 @@ public function testDefaultParamIsAddedToMatch() $stack->addRoute('foo', new TestAsset\DummyRoute()); $stack->setDefaultParam('foo', 'bar'); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $result = $stack->match(new ServerRequest()); + $this->assertTrue($result->isSuccess()); + $this->assertArraySubset(['foo' => 'bar'], $result->getMatchedParams()); } public function testDefaultParamDoesNotOverrideParam() @@ -227,45 +116,38 @@ public function testDefaultParamDoesNotOverrideParam() $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); $stack->setDefaultParam('foo', 'baz'); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $result = $stack->match(new ServerRequest()); + $this->assertTrue($result->isSuccess()); + $this->assertArraySubset(['foo' => 'bar'], $result->getMatchedParams()); } public function testDefaultParamIsUsedForAssembling() { + $uri = new Uri(); $stack = new SimpleRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); $stack->setDefaultParam('foo', 'bar'); - $this->assertEquals('bar', $stack->assemble([], ['name' => 'foo'])); + $this->assertEquals('bar', $stack->assemble($uri, [], ['name' => 'foo'])->getPath()); } public function testDefaultParamDoesNotOverrideParamForAssembling() { + $uri = new Uri(); $stack = new SimpleRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); $stack->setDefaultParam('foo', 'baz'); - $this->assertEquals('bar', $stack->assemble(['foo' => 'bar'], ['name' => 'foo'])); - } - - public function testFactory() - { - $tester = new FactoryTester($this); - $tester->testFactory( - SimpleRouteStack::class, - [], - [ - 'route_plugins' => new RoutePluginManager(new ServiceManager()), - 'routes' => [], - 'default_params' => [] - ] - ); + $this->assertEquals('bar', $stack->assemble($uri, ['foo' => 'bar'], ['name' => 'foo'])->getPath()); } public function testGetRoutes() { $stack = new SimpleRouteStack(); - $this->assertInstanceOf('Traversable', $stack->getRoutes()); + + $route = new TestAsset\DummyRoute(); + $stack->addRoute('foo', $route); + $this->assertEquals(['foo' => $route], $stack->getRoutes()); } public function testGetRouteByName() diff --git a/test/TestAsset/DummyRoute.php b/test/TestAsset/DummyRoute.php index c6d3ede..d53911d 100644 --- a/test/TestAsset/DummyRoute.php +++ b/test/TestAsset/DummyRoute.php @@ -9,48 +9,23 @@ namespace ZendTest\Router\TestAsset; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; use Zend\Router\RouteInterface; -use Zend\Router\RouteMatch; -use Zend\Stdlib\RequestInterface; +use Zend\Router\RouteResult; /** * Dummy route. */ class DummyRoute implements RouteInterface { - /** - * match(): defined by RouteInterface interface. - * - * @see Route::match() - * @param RequestInterface $request - * @return RouteMatch - */ - public function match(RequestInterface $request) + public function match(Request $request, int $pathOffset = 0, array $options = []) : RouteResult { - return new RouteMatch([]); + return RouteResult::fromRouteMatch([]); } - /** - * assemble(): defined by RouteInterface interface. - * - * @see Route::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = null, array $options = null) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { - return ''; - } - - /** - * factory(): defined by RouteInterface interface - * - * @param array|\Traversable $options - * @return DummyRoute - */ - public static function factory($options = []) - { - return new static(); + return $uri; } } diff --git a/test/TestAsset/DummyRouteWithParam.php b/test/TestAsset/DummyRouteWithParam.php index b3a3994..e43f59c 100644 --- a/test/TestAsset/DummyRouteWithParam.php +++ b/test/TestAsset/DummyRouteWithParam.php @@ -9,40 +9,27 @@ namespace ZendTest\Router\TestAsset; -use Zend\Router\RouteMatch; -use Zend\Stdlib\RequestInterface; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; +use Zend\Router\RouteInterface; +use Zend\Router\RouteResult; /** * Dummy route. */ -class DummyRouteWithParam extends DummyRoute +class DummyRouteWithParam implements RouteInterface { - /** - * match(): defined by RouteInterface interface. - * - * @see Route::match() - * @param RequestInterface $request - * @return RouteMatch - */ - public function match(RequestInterface $request) + public function match(Request $request, int $pathOffset = 0, array $options = []) : RouteResult { - return new RouteMatch(['foo' => 'bar']); + return RouteResult::fromRouteMatch(['foo' => 'bar']); } - /** - * assemble(): defined by RouteInterface interface. - * - * @see Route::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = null, array $options = null) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { if (isset($params['foo'])) { - return $params['foo']; + return $uri->withPath($params['foo']); } - return ''; + return $uri; } } diff --git a/test/TreeRouteStackTest.php b/test/TreeRouteStackTest.php index 74f74bc..9350b4d 100644 --- a/test/TreeRouteStackTest.php +++ b/test/TreeRouteStackTest.php @@ -1,7 +1,7 @@ expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Route definition must be an array or Traversable object'); - $stack->addRoute('foo', new \ZendTest\Router\TestAsset\DummyRoute()); - } - - public function testAddRouteViaStringRequiresHttpSpecificRoute() - { - $stack = new TreeRouteStack(); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Given route does not implement HTTP route interface'); - $stack->addRoute('foo', [ - 'type' => \ZendTest\Router\TestAsset\DummyRoute::class - ]); - } - - public function testAddRouteAcceptsTraversable() - { - $stack = new TreeRouteStack(); - $stack->addRoute('foo', new ArrayIterator([ - 'type' => TestAsset\DummyRoute::class - ])); - } - - public function testNoMatchWithoutUriMethod() - { - $stack = new TreeRouteStack(); - $request = new BaseRequest(); - - $this->assertNull($stack->match($request)); - } - - public function testSetBaseUrlFromFirstMatch() - { - $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->setBaseUrl('/foo'); - $stack->addRoute('foo', [ - 'type' => TestAsset\DummyRoute::class - ]); - - $this->assertEquals(4, $stack->match(new Request())->getParam('offset')); - } - - public function testNoOffsetIsPassedWithoutBaseUrl() - { - $stack = new TreeRouteStack(); - $stack->addRoute('foo', [ - 'type' => TestAsset\DummyRoute::class - ]); - - $this->assertEquals(null, $stack->match(new Request())->getParam('offset')); - } - public function testAssemble() { + $uri = new Uri(); $stack = new TreeRouteStack(); $stack->addRoute('foo', new TestAsset\DummyRoute()); - $this->assertEquals('', $stack->assemble([], ['name' => 'foo'])); - } - - public function testAssembleCanonicalUriWithoutRequestUri() - { - $stack = new TreeRouteStack(); - $stack->addRoute('foo', new TestAsset\DummyRoute()); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Request URI has not been set'); - $stack->assemble([], ['name' => 'foo', 'force_canonical' => true]); - } - - public function testAssembleCanonicalUriWithRequestUri() - { - $uri = new HttpUri('http://example.com:8080/'); - $stack = new TreeRouteStack(); - $stack->setRequestUri($uri); - - $stack->addRoute('foo', new TestAsset\DummyRoute()); - $this->assertEquals( - 'http://example.com:8080/', - $stack->assemble([], ['name' => 'foo', 'force_canonical' => true]) - ); - } - - public function testAssembleCanonicalUriWithGivenUri() - { - $uri = new HttpUri('http://example.com:8080/'); - $stack = new TreeRouteStack(); - - $stack->addRoute('foo', new TestAsset\DummyRoute()); - $this->assertEquals( - 'http://example.com:8080/', - $stack->assemble([], ['name' => 'foo', 'uri' => $uri, 'force_canonical' => true]) - ); + $this->assertEquals('', $stack->assemble($uri, [], ['name' => 'foo'])->getPath()); } public function testAssembleCanonicalUriWithHostnameRoute() { $stack = new TreeRouteStack(); $stack->addRoute('foo', new Hostname('example.com')); - $uri = new HttpUri(); - $uri->setScheme('http'); + $uri = new Uri(); + $uri = $uri->withScheme('http'); - $this->assertEquals('http://example.com/', $stack->assemble([], ['name' => 'foo', 'uri' => $uri])); + $this->assertEquals( + 'http://example.com', + $stack->assemble($uri, [], ['name' => 'foo'])->__toString() + ); } public function testAssembleCanonicalUriWithHostnameRouteWithoutScheme() { $stack = new TreeRouteStack(); $stack->addRoute('foo', new Hostname('example.com')); - $uri = new HttpUri(); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Request URI has not been set'); - $stack->assemble([], ['name' => 'foo', 'uri' => $uri]); - } - - public function testAssembleCanonicalUriWithHostnameRouteAndRequestUriWithoutScheme() - { - $uri = new HttpUri(); - $uri->setScheme('http'); - $stack = new TreeRouteStack(); - $stack->setRequestUri($uri); - $stack->addRoute('foo', new Hostname('example.com')); - - $this->assertEquals('http://example.com/', $stack->assemble([], ['name' => 'foo'])); - } + $uri = new Uri(); - public function testAssembleWithQueryParams() - { - $stack = new TreeRouteStack(); - $stack->addRoute( - 'index', - [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/', - ], - ] + $this->assertEquals( + '//example.com', + $stack->assemble($uri, [], ['name' => 'foo'])->__toString() ); - - $this->assertEquals('/?foo=bar', $stack->assemble([], ['name' => 'index', 'query' => ['foo' => 'bar']])); } public function testAssembleWithEncodedPath() { + $uri = new Uri(); $stack = new TreeRouteStack(); - $stack->addRoute( - 'index', - [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/this%2Fthat', - ], - ] - ); - - $this->assertEquals('/this%2Fthat', $stack->assemble([], ['name' => 'index'])); - } - - public function testAssembleWithEncodedPathAndQueryParams() - { - $stack = new TreeRouteStack(); - $stack->addRoute( - 'index', - [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/this%2Fthat', - ], - ] - ); + $stack->addRoute('index', new Literal('/this%2Fthat')); - $this->assertEquals( - '/this%2Fthat?foo=bar', - $stack->assemble([], ['name' => 'index', 'query' => ['foo' => 'bar'], 'normalize_path' => false]) - ); + $this->assertEquals('/this%2Fthat', $stack->assemble($uri, [], ['name' => 'index'])->getPath()); } public function testAssembleWithScheme() { - $uri = new HttpUri(); - $uri->setScheme('http'); - $uri->setHost('example.com'); + $uri = new Uri(); + $uri = $uri->withScheme('http'); + $uri = $uri->withHost('example.com'); $stack = new TreeRouteStack(); - $stack->setRequestUri($uri); $stack->addRoute( 'secure', - [ - 'type' => 'Scheme', - 'options' => [ - 'scheme' => 'https' - ], + Part::factory([ + 'route' => new Scheme('https'), 'child_routes' => [ - 'index' => [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/', - ], - ], + 'index' => new Literal('/'), ], - ] + ]) ); - $this->assertEquals('https://example.com/', $stack->assemble([], ['name' => 'secure/index'])); - } - - public function testAssembleWithFragment() - { - $stack = new TreeRouteStack(); - $stack->addRoute( - 'index', - [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/', - ], - ] + $this->assertEquals( + 'https://example.com/', + $stack->assemble($uri, [], ['name' => 'secure/index'])->__toString() ); - - $this->assertEquals('/#foobar', $stack->assemble([], ['name' => 'index', 'fragment' => 'foobar'])); } public function testAssembleWithoutNameOption() { + $uri = new Uri(); $stack = new TreeRouteStack(); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing "name" option'); - $stack->assemble(); + $stack->assemble($uri); } public function testAssembleNonExistentRoute() { + $uri = new Uri(); $stack = new TreeRouteStack(); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Route with name "foo" not found'); - $stack->assemble([], ['name' => 'foo']); + $stack->assemble($uri, [], ['name' => 'foo']); } public function testAssembleNonExistentChildRoute() { + $uri = new Uri(); $stack = new TreeRouteStack(); - $stack->addRoute( - 'index', - [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/', - ], - ] - ); + $stack->addRoute('index', new Literal('/')); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Route with name "index" does not have child routes'); - $stack->assemble([], ['name' => 'index/foo']); + $stack->assemble($uri, [], ['name' => 'index/foo']); } public function testDefaultParamIsAddedToMatch() { $stack = new TreeRouteStack(); - $stack->setBaseUrl('/foo'); - $stack->addRoute('foo', new TestAsset\DummyRoute()); + $request = new ServerRequest(); + $route = $this->prophesize(RouteInterface::class); + $route->match($request, Argument::any(), Argument::any()) + ->willReturn(RouteResult::fromRouteMatch([])); + $stack->addRoute('foo', $route->reveal()); $stack->setDefaultParam('foo', 'bar'); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $result = $stack->match($request); + $this->assertTrue($result->isSuccess()); + $this->assertArraySubset(['foo' => 'bar'], $result->getMatchedParams()); } - public function testDefaultParamDoesNotOverrideParam() + public function testMethodFailureVerbsAreCombined() { $stack = new TreeRouteStack(); - $stack->setBaseUrl('/foo'); - $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); - $stack->setDefaultParam('foo', 'baz'); + $stack->addRoute('foo', new Method('POST,DELETE')); + $stack->addRoute('bar', new Method('GET,POST')); - $this->assertEquals('bar', $stack->match(new Request())->getParam('foo')); + $request = new ServerRequest([], [], new Uri('/'), 'PUT'); + $result = $stack->match($request, 1); + $this->assertTrue($result->isMethodFailure()); + $this->assertEquals(['GET', 'POST', 'DELETE'], $result->getAllowedMethods()); } - public function testDefaultParamIsUsedForAssembling() + public function testRoutingFailure() { $stack = new TreeRouteStack(); - $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); - $stack->setDefaultParam('foo', 'bar'); + $stack->addRoute('foo', new Literal('/foo')); - $this->assertEquals('bar', $stack->assemble([], ['name' => 'foo'])); + $request = new ServerRequest([], [], new Uri('/bar')); + $result = $stack->match($request); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); } - public function testDefaultParamDoesNotOverrideParamForAssembling() + public function testDefaultParamDoesNotOverrideMatchParam() { $stack = new TreeRouteStack(); - $stack->addRoute('foo', new TestAsset\DummyRouteWithParam()); + $route = $this->prophesize(RouteInterface::class); + $route->match(Argument::any(), Argument::any(), Argument::any()) + ->willReturn(RouteResult::fromRouteMatch(['foo' => 'bar'])); + $stack->addRoute('foo', $route->reveal()); $stack->setDefaultParam('foo', 'baz'); - $this->assertEquals('bar', $stack->assemble(['foo' => 'bar'], ['name' => 'foo'])); + $result = $stack->match(new ServerRequest()); + $this->assertTrue($result->isSuccess()); + $this->assertArraySubset(['foo' => 'bar'], $result->getMatchedParams()); } - public function testSetBaseUrl() + public function testDefaultParamIsUsedForAssembling() { + $uri = new Uri(); $stack = new TreeRouteStack(); + $route = $this->prophesize(RouteInterface::class); + $route->assemble($uri, ['foo' => 'bar'], []) + ->shouldBeCalled(); + $stack->addRoute('foo', $route->reveal()); + $stack->setDefaultParam('foo', 'bar'); - $this->assertEquals($stack, $stack->setBaseUrl('/foo/')); - $this->assertEquals('/foo', $stack->getBaseUrl()); + $stack->assemble($uri, [], ['name' => 'foo']); } - public function testSetRequestUri() + public function testDefaultParamDoesNotOverrideParamForAssembling() { - $uri = new HttpUri(); + $uri = new Uri(); $stack = new TreeRouteStack(); + $route = $this->prophesize(RouteInterface::class); + $route->assemble($uri, ['foo' => 'bar'], []) + ->shouldBeCalled(); + $stack->addRoute('foo', $route->reveal()); + $stack->setDefaultParam('foo', 'baz'); - $this->assertEquals($stack, $stack->setRequestUri($uri)); - $this->assertEquals($uri, $stack->getRequestUri()); + $stack->assemble($uri, ['foo' => 'bar'], ['name' => 'foo']); } public function testPriorityIsPassedToPartRoute() { $stack = new TreeRouteStack(); - $stack->addRoutes([ - 'foo' => [ - 'type' => 'Literal', - 'priority' => 1000, - 'options' => [ - 'route' => '/foo', - 'defaults' => [ - 'controller' => 'foo', - ], - ], - 'may_terminate' => true, - 'child_routes' => [ - 'bar' => [ - 'type' => 'Literal', - 'options' => [ - 'route' => '/bar', - 'defaults' => [ - 'controller' => 'foo', - 'action' => 'bar', - ], - ], - ], - ], + $stack->addRoute('foo', Part::factory([ + 'route' => new Literal('/foo', ['controller' => 'foo']), + 'may_terminate' => true, + 'child_routes' => [ + 'bar' => new Literal('/bar', ['controller' => 'foo', 'action' => 'bar']), ], - ]); + ]), 1000); - $reflectedClass = new \ReflectionClass($stack); + $reflectedClass = new ReflectionClass($stack); $reflectedProperty = $reflectedClass->getProperty('routes'); $reflectedProperty->setAccessible(true); $routes = $reflectedProperty->getValue($stack); - $this->assertEquals(1000, $routes->get('foo')->priority); - } - - public function testPrototypeRoute() - { - $stack = new TreeRouteStack(); - $stack->addPrototype( - 'bar', - ['type' => 'literal', 'options' => ['route' => '/bar']] - ); - $stack->addRoute('foo', 'bar'); - $this->assertEquals('/bar', $stack->assemble([], ['name' => 'foo'])); - } - - public function testChainRouteAssembling() - { - $stack = new TreeRouteStack(); - $stack->addPrototype( - 'bar', - ['type' => 'literal', 'options' => ['route' => '/bar']] - ); - $stack->addRoute( - 'foo', - [ - 'type' => 'literal', - 'options' => [ - 'route' => '/foo' - ], - 'chain_routes' => [ - 'bar' - ], - ] - ); - $this->assertEquals('/foo/bar', $stack->assemble([], ['name' => 'foo'])); - } - - public function testChainRouteAssemblingWithChildrenAndSecureScheme() - { - $stack = new TreeRouteStack(); - - $uri = new \Zend\Uri\Http(); - $uri->setHost('localhost'); - - $stack->setRequestUri($uri); - $stack->addRoute( - 'foo', - [ - 'type' => 'literal', - 'options' => [ - 'route' => '/foo' - ], - 'chain_routes' => [ - ['type' => 'scheme', 'options' => ['scheme' => 'https']] - ], - 'child_routes' => [ - 'baz' => [ - 'type' => 'literal', - 'options' => [ - 'route' => '/baz' - ], - ] - ] - ] - ); - $this->assertEquals('https://localhost/foo/baz', $stack->assemble([], ['name' => 'foo/baz'])); - } - - public function testFactory() - { - $tester = new FactoryTester($this); - $tester->testFactory( - TreeRouteStack::class, - [], - [] - ); + $this->assertEquals(1000, $routes->toArray(PriorityList::EXTR_PRIORITY)['foo']); } } From 1de5477aef011ec2b7ed84760a3ceae6d9b6c6f9 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sun, 4 Mar 2018 15:10:58 +1000 Subject: [PATCH 14/47] Drop translator aware route stack --- src/Http/TranslatorAwareTreeRouteStack.php | 178 ------------------ .../TranslatorAwareTreeRouteStackTest.php | 168 ----------------- 2 files changed, 346 deletions(-) delete mode 100644 src/Http/TranslatorAwareTreeRouteStack.php delete mode 100644 test/Http/TranslatorAwareTreeRouteStackTest.php diff --git a/src/Http/TranslatorAwareTreeRouteStack.php b/src/Http/TranslatorAwareTreeRouteStack.php deleted file mode 100644 index 5f95446..0000000 --- a/src/Http/TranslatorAwareTreeRouteStack.php +++ /dev/null @@ -1,178 +0,0 @@ -hasTranslator() && $this->isTranslatorEnabled() && ! isset($options['translator'])) { - $options['translator'] = $this->getTranslator(); - } - - if (! isset($options['text_domain'])) { - $options['text_domain'] = $this->getTranslatorTextDomain(); - } - - return parent::match($request, $pathOffset, $options); - } - - /** - * assemble(): defined by \Zend\Router\RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - * @throws Exception\InvalidArgumentException - * @throws Exception\RuntimeException - */ - public function assemble(array $params = [], array $options = []) - { - if ($this->hasTranslator() && $this->isTranslatorEnabled() && ! isset($options['translator'])) { - $options['translator'] = $this->getTranslator(); - } - - if (! isset($options['text_domain'])) { - $options['text_domain'] = $this->getTranslatorTextDomain(); - } - - return parent::assemble($params, $options); - } - - /** - * setTranslator(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::setTranslator() - * @param Translator $translator - * @param string $textDomain - * @return TreeRouteStack - */ - public function setTranslator(Translator $translator = null, $textDomain = null) - { - $this->translator = $translator; - - if ($textDomain !== null) { - $this->setTranslatorTextDomain($textDomain); - } - - return $this; - } - - /** - * getTranslator(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::getTranslator() - * @return Translator - */ - public function getTranslator() - { - return $this->translator; - } - - /** - * hasTranslator(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::hasTranslator() - * @return bool - */ - public function hasTranslator() - { - return $this->translator !== null; - } - - /** - * setTranslatorEnabled(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::setTranslatorEnabled() - * @param bool $enabled - * @return TreeRouteStack - */ - public function setTranslatorEnabled($enabled = true) - { - $this->translatorEnabled = $enabled; - return $this; - } - - /** - * isTranslatorEnabled(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::isTranslatorEnabled() - * @return bool - */ - public function isTranslatorEnabled() - { - return $this->translatorEnabled; - } - - /** - * setTranslatorTextDomain(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::setTranslatorTextDomain() - * @param string $textDomain - * @return self - */ - public function setTranslatorTextDomain($textDomain = 'default') - { - $this->translatorTextDomain = $textDomain; - - return $this; - } - - /** - * getTranslatorTextDomain(): defined by TranslatorAwareInterface. - * - * @see TranslatorAwareInterface::getTranslatorTextDomain() - * @return string - */ - public function getTranslatorTextDomain() - { - return $this->translatorTextDomain; - } -} diff --git a/test/Http/TranslatorAwareTreeRouteStackTest.php b/test/Http/TranslatorAwareTreeRouteStackTest.php deleted file mode 100644 index 03eba53..0000000 --- a/test/Http/TranslatorAwareTreeRouteStackTest.php +++ /dev/null @@ -1,168 +0,0 @@ -markTestIncomplete('Re-enable once zend-i18n is updated to zend-servicemanager v3'); - - $this->testFilesDir = __DIR__ . '/_files'; - - $this->translator = new Translator(); - $this->translator->addTranslationFile('phpArray', $this->testFilesDir . '/tokens.en.php', 'route', 'en'); - $this->translator->addTranslationFile('phpArray', $this->testFilesDir . '/tokens.de.php', 'route', 'de'); - - $this->fooRoute = [ - 'type' => 'Segment', - 'options' => [ - 'route' => '/:locale', - ], - 'child_routes' => [ - 'index' => [ - 'type' => 'Segment', - 'options' => [ - 'route' => '/{homepage}', - ], - ], - ], - ]; - } - - public function testTranslatorAwareInterfaceImplementation() - { - $stack = new TranslatorAwareTreeRouteStack(); - $this->assertInstanceOf(TranslatorAwareInterface::class, $stack); - - // Defaults - $this->assertNull($stack->getTranslator()); - $this->assertFalse($stack->hasTranslator()); - $this->assertEquals('default', $stack->getTranslatorTextDomain()); - $this->assertTrue($stack->isTranslatorEnabled()); - - // Inject translator without text domain - $translator = new Translator(); - $stack->setTranslator($translator); - $this->assertSame($translator, $stack->getTranslator()); - $this->assertEquals('default', $stack->getTranslatorTextDomain()); - $this->assertTrue($stack->hasTranslator()); - - // Reset translator - $stack->setTranslator(null); - $this->assertNull($stack->getTranslator()); - $this->assertFalse($stack->hasTranslator()); - - // Inject translator with text domain - $stack->setTranslator($translator, 'alternative'); - $this->assertSame($translator, $stack->getTranslator()); - $this->assertEquals('alternative', $stack->getTranslatorTextDomain()); - - // Set text domain - $stack->setTranslatorTextDomain('default'); - $this->assertEquals('default', $stack->getTranslatorTextDomain()); - - // Disable translator - $stack->setTranslatorEnabled(false); - $this->assertFalse($stack->isTranslatorEnabled()); - } - - public function testTranslatorIsPassedThroughMatchMethod() - { - $translator = new Translator(); - $request = new Request(); - - $route = $this->getMock(RouteInterface::class); - $route->expects($this->once()) - ->method('match') - ->with( - $this->equalTo($request), - $this->isNull(), - $this->equalTo(['translator' => $translator, 'text_domain' => 'default']) - ); - - $stack = new TranslatorAwareTreeRouteStack(); - $stack->addRoute('test', $route); - - $stack->match($request, null, ['translator' => $translator]); - } - - public function testTranslatorIsPassedThroughAssembleMethod() - { - $translator = new Translator(); - $uri = new HttpUri(); - - $route = $this->getMock(RouteInterface::class); - $route->expects($this->once()) - ->method('assemble') - ->with( - $this->equalTo([]), - $this->equalTo(['translator' => $translator, 'text_domain' => 'default', 'uri' => $uri]) - ); - - $stack = new TranslatorAwareTreeRouteStack(); - $stack->addRoute('test', $route); - - $stack->assemble([], ['name' => 'test', 'translator' => $translator, 'uri' => $uri]); - } - - public function testAssembleRouteWithParameterLocale() - { - $stack = new TranslatorAwareTreeRouteStack(); - $stack->setTranslator($this->translator, 'route'); - $stack->addRoute( - 'foo', - $this->fooRoute - ); - - $this->assertEquals('/de/hauptseite', $stack->assemble(['locale' => 'de'], ['name' => 'foo/index'])); - $this->assertEquals('/en/homepage', $stack->assemble(['locale' => 'en'], ['name' => 'foo/index'])); - } - - public function testMatchRouteWithParameterLocale() - { - $stack = new TranslatorAwareTreeRouteStack(); - $stack->setTranslator($this->translator, 'route'); - $stack->addRoute( - 'foo', - $this->fooRoute - ); - - $request = new Request(); - $request->setUri('http://example.com/de/hauptseite'); - - $match = $stack->match($request); - $this->assertNotNull($match); - $this->assertEquals('foo/index', $match->getMatchedRouteName()); - } -} From 6dce941e9cc50ec4c1c1655989bba30d07543f98 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sun, 4 Mar 2018 17:01:57 +1000 Subject: [PATCH 15/47] Rename Zend\Router\Http namespace to Zend\Router\Route: move files --- src/{Http => Route}/Chain.php | 0 src/{Http => Route}/Hostname.php | 0 src/{Http => Route}/HttpRouterFactory.php | 0 src/{Http => Route}/Literal.php | 0 src/{Http => Route}/Method.php | 0 src/{Http => Route}/Part.php | 0 src/{Http => Route}/Regex.php | 0 src/{Http => Route}/RouteMatch.php | 0 src/{Http => Route}/Scheme.php | 0 src/{Http => Route}/Segment.php | 0 test/{Http => Route}/ChainTest.php | 0 test/{Http => Route}/HostnameTest.php | 0 test/{Http => Route}/HttpRouterFactoryTest.php | 0 test/{Http => Route}/LiteralTest.php | 0 test/{Http => Route}/MethodTest.php | 0 test/{Http => Route}/PartTest.php | 0 test/{Http => Route}/RegexTest.php | 0 test/{Http => Route}/RouteMatchTest.php | 0 test/{Http => Route}/SchemeTest.php | 0 test/{Http => Route}/SegmentTest.php | 0 test/{Http => Route}/TestAsset/DummyRoute.php | 0 test/{Http => Route}/TestAsset/DummyRouteWithParam.php | 0 test/{Http => Route}/_files/tokens.de.php | 0 test/{Http => Route}/_files/tokens.en.php | 0 24 files changed, 0 insertions(+), 0 deletions(-) rename src/{Http => Route}/Chain.php (100%) rename src/{Http => Route}/Hostname.php (100%) rename src/{Http => Route}/HttpRouterFactory.php (100%) rename src/{Http => Route}/Literal.php (100%) rename src/{Http => Route}/Method.php (100%) rename src/{Http => Route}/Part.php (100%) rename src/{Http => Route}/Regex.php (100%) rename src/{Http => Route}/RouteMatch.php (100%) rename src/{Http => Route}/Scheme.php (100%) rename src/{Http => Route}/Segment.php (100%) rename test/{Http => Route}/ChainTest.php (100%) rename test/{Http => Route}/HostnameTest.php (100%) rename test/{Http => Route}/HttpRouterFactoryTest.php (100%) rename test/{Http => Route}/LiteralTest.php (100%) rename test/{Http => Route}/MethodTest.php (100%) rename test/{Http => Route}/PartTest.php (100%) rename test/{Http => Route}/RegexTest.php (100%) rename test/{Http => Route}/RouteMatchTest.php (100%) rename test/{Http => Route}/SchemeTest.php (100%) rename test/{Http => Route}/SegmentTest.php (100%) rename test/{Http => Route}/TestAsset/DummyRoute.php (100%) rename test/{Http => Route}/TestAsset/DummyRouteWithParam.php (100%) rename test/{Http => Route}/_files/tokens.de.php (100%) rename test/{Http => Route}/_files/tokens.en.php (100%) diff --git a/src/Http/Chain.php b/src/Route/Chain.php similarity index 100% rename from src/Http/Chain.php rename to src/Route/Chain.php diff --git a/src/Http/Hostname.php b/src/Route/Hostname.php similarity index 100% rename from src/Http/Hostname.php rename to src/Route/Hostname.php diff --git a/src/Http/HttpRouterFactory.php b/src/Route/HttpRouterFactory.php similarity index 100% rename from src/Http/HttpRouterFactory.php rename to src/Route/HttpRouterFactory.php diff --git a/src/Http/Literal.php b/src/Route/Literal.php similarity index 100% rename from src/Http/Literal.php rename to src/Route/Literal.php diff --git a/src/Http/Method.php b/src/Route/Method.php similarity index 100% rename from src/Http/Method.php rename to src/Route/Method.php diff --git a/src/Http/Part.php b/src/Route/Part.php similarity index 100% rename from src/Http/Part.php rename to src/Route/Part.php diff --git a/src/Http/Regex.php b/src/Route/Regex.php similarity index 100% rename from src/Http/Regex.php rename to src/Route/Regex.php diff --git a/src/Http/RouteMatch.php b/src/Route/RouteMatch.php similarity index 100% rename from src/Http/RouteMatch.php rename to src/Route/RouteMatch.php diff --git a/src/Http/Scheme.php b/src/Route/Scheme.php similarity index 100% rename from src/Http/Scheme.php rename to src/Route/Scheme.php diff --git a/src/Http/Segment.php b/src/Route/Segment.php similarity index 100% rename from src/Http/Segment.php rename to src/Route/Segment.php diff --git a/test/Http/ChainTest.php b/test/Route/ChainTest.php similarity index 100% rename from test/Http/ChainTest.php rename to test/Route/ChainTest.php diff --git a/test/Http/HostnameTest.php b/test/Route/HostnameTest.php similarity index 100% rename from test/Http/HostnameTest.php rename to test/Route/HostnameTest.php diff --git a/test/Http/HttpRouterFactoryTest.php b/test/Route/HttpRouterFactoryTest.php similarity index 100% rename from test/Http/HttpRouterFactoryTest.php rename to test/Route/HttpRouterFactoryTest.php diff --git a/test/Http/LiteralTest.php b/test/Route/LiteralTest.php similarity index 100% rename from test/Http/LiteralTest.php rename to test/Route/LiteralTest.php diff --git a/test/Http/MethodTest.php b/test/Route/MethodTest.php similarity index 100% rename from test/Http/MethodTest.php rename to test/Route/MethodTest.php diff --git a/test/Http/PartTest.php b/test/Route/PartTest.php similarity index 100% rename from test/Http/PartTest.php rename to test/Route/PartTest.php diff --git a/test/Http/RegexTest.php b/test/Route/RegexTest.php similarity index 100% rename from test/Http/RegexTest.php rename to test/Route/RegexTest.php diff --git a/test/Http/RouteMatchTest.php b/test/Route/RouteMatchTest.php similarity index 100% rename from test/Http/RouteMatchTest.php rename to test/Route/RouteMatchTest.php diff --git a/test/Http/SchemeTest.php b/test/Route/SchemeTest.php similarity index 100% rename from test/Http/SchemeTest.php rename to test/Route/SchemeTest.php diff --git a/test/Http/SegmentTest.php b/test/Route/SegmentTest.php similarity index 100% rename from test/Http/SegmentTest.php rename to test/Route/SegmentTest.php diff --git a/test/Http/TestAsset/DummyRoute.php b/test/Route/TestAsset/DummyRoute.php similarity index 100% rename from test/Http/TestAsset/DummyRoute.php rename to test/Route/TestAsset/DummyRoute.php diff --git a/test/Http/TestAsset/DummyRouteWithParam.php b/test/Route/TestAsset/DummyRouteWithParam.php similarity index 100% rename from test/Http/TestAsset/DummyRouteWithParam.php rename to test/Route/TestAsset/DummyRouteWithParam.php diff --git a/test/Http/_files/tokens.de.php b/test/Route/_files/tokens.de.php similarity index 100% rename from test/Http/_files/tokens.de.php rename to test/Route/_files/tokens.de.php diff --git a/test/Http/_files/tokens.en.php b/test/Route/_files/tokens.en.php similarity index 100% rename from test/Http/_files/tokens.en.php rename to test/Route/_files/tokens.en.php From a25f0c5fbcd0cdc935633fe98b57e55ef7d98f2f Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sun, 4 Mar 2018 17:05:38 +1000 Subject: [PATCH 16/47] Rename Zend\Router\Http namespace to Zend\Router\Route --- src/ConfigProvider.php | 2 +- src/Route/Chain.php | 2 +- src/Route/Hostname.php | 2 +- src/Route/HttpRouterFactory.php | 2 +- src/Route/Literal.php | 2 +- src/Route/Method.php | 2 +- src/Route/Part.php | 2 +- src/Route/Regex.php | 2 +- src/Route/RouteMatch.php | 2 +- src/Route/Scheme.php | 2 +- src/Route/Segment.php | 2 +- test/Route/ChainTest.php | 10 +++++----- test/Route/HostnameTest.php | 6 +++--- test/Route/HttpRouterFactoryTest.php | 4 ++-- test/Route/LiteralTest.php | 6 +++--- test/Route/MethodTest.php | 6 +++--- test/Route/PartTest.php | 14 +++++++------- test/Route/RegexTest.php | 6 +++--- test/Route/RouteMatchTest.php | 4 ++-- test/Route/SchemeTest.php | 6 +++--- test/Route/SegmentTest.php | 6 +++--- test/Route/TestAsset/DummyRoute.php | 6 +++--- test/Route/TestAsset/DummyRouteWithParam.php | 4 ++-- test/RouterFactoryTest.php | 2 +- 24 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index b3e3a17..a99d6e9 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -47,7 +47,7 @@ public function getDependencyConfig() 'RoutePluginManager' => RoutePluginManager::class, ], 'factories' => [ - TreeRouteStack::class => Http\HttpRouterFactory::class, + TreeRouteStack::class => Route\HttpRouterFactory::class, RoutePluginManager::class => RoutePluginManagerFactory::class, RouteStackInterface::class => RouterFactory::class, ], diff --git a/src/Route/Chain.php b/src/Route/Chain.php index f45c43f..c34f988 100644 --- a/src/Route/Chain.php +++ b/src/Route/Chain.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use ArrayObject; use Traversable; diff --git a/src/Route/Hostname.php b/src/Route/Hostname.php index b2e2b7b..2f4c880 100644 --- a/src/Route/Hostname.php +++ b/src/Route/Hostname.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use Traversable; use Zend\Router\Exception; diff --git a/src/Route/HttpRouterFactory.php b/src/Route/HttpRouterFactory.php index 990d101..e4ef606 100644 --- a/src/Route/HttpRouterFactory.php +++ b/src/Route/HttpRouterFactory.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use Interop\Container\ContainerInterface; use Zend\Router\RouterConfigTrait; diff --git a/src/Route/Literal.php b/src/Route/Literal.php index 4fe2ac6..67f3094 100644 --- a/src/Route/Literal.php +++ b/src/Route/Literal.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use Traversable; use Zend\Router\Exception; diff --git a/src/Route/Method.php b/src/Route/Method.php index 454cdf6..ba6efad 100644 --- a/src/Route/Method.php +++ b/src/Route/Method.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use Traversable; use Zend\Router\Exception; diff --git a/src/Route/Part.php b/src/Route/Part.php index ca52b54..aaa4d0d 100644 --- a/src/Route/Part.php +++ b/src/Route/Part.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use ArrayObject; use Traversable; diff --git a/src/Route/Regex.php b/src/Route/Regex.php index 99f9840..1a1559e 100644 --- a/src/Route/Regex.php +++ b/src/Route/Regex.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use Traversable; use Zend\Router\Exception; diff --git a/src/Route/RouteMatch.php b/src/Route/RouteMatch.php index d3dbbfc..cfabd1b 100644 --- a/src/Route/RouteMatch.php +++ b/src/Route/RouteMatch.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use Zend\Router\RouteMatch as BaseRouteMatch; diff --git a/src/Route/Scheme.php b/src/Route/Scheme.php index 463050e..481cb92 100644 --- a/src/Route/Scheme.php +++ b/src/Route/Scheme.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use Traversable; use Zend\Router\Exception; diff --git a/src/Route/Segment.php b/src/Route/Segment.php index 4f88a14..a90607f 100644 --- a/src/Route/Segment.php +++ b/src/Route/Segment.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Zend\Router\Http; +namespace Zend\Router\Route; use Traversable; use Zend\I18n\Translator\TranslatorInterface as Translator; diff --git a/test/Route/ChainTest.php b/test/Route/ChainTest.php index aa9b413..f2498d5 100644 --- a/test/Route/ChainTest.php +++ b/test/Route/ChainTest.php @@ -7,14 +7,14 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; use Zend\Http\Request; -use Zend\Router\Http\Chain; -use Zend\Router\Http\RouteMatch; -use Zend\Router\Http\Segment; -use Zend\Router\Http\Wildcard; +use Zend\Router\Route\Chain; +use Zend\Router\Route\RouteMatch; +use Zend\Router\Route\Segment; +use Zend\Router\Route\Wildcard; use Zend\Router\RoutePluginManager; use Zend\ServiceManager\ServiceManager; use ZendTest\Router\FactoryTester; diff --git a/test/Route/HostnameTest.php b/test/Route/HostnameTest.php index 25fc09d..e56214c 100644 --- a/test/Route/HostnameTest.php +++ b/test/Route/HostnameTest.php @@ -7,14 +7,14 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; use Zend\Http\Request; use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Exception\RuntimeException; -use Zend\Router\Http\Hostname; -use Zend\Router\Http\RouteMatch; +use Zend\Router\Route\Hostname; +use Zend\Router\Route\RouteMatch; use Zend\Stdlib\Request as BaseRequest; use Zend\Uri\Http as HttpUri; use ZendTest\Router\FactoryTester; diff --git a/test/Route/HttpRouterFactoryTest.php b/test/Route/HttpRouterFactoryTest.php index ff546d6..b08e890 100644 --- a/test/Route/HttpRouterFactoryTest.php +++ b/test/Route/HttpRouterFactoryTest.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; -use Zend\Router\Http\HttpRouterFactory; +use Zend\Router\Route\HttpRouterFactory; use Zend\Router\RoutePluginManager; use ZendTest\Router\RouterFactoryTest as TestCase; diff --git a/test/Route/LiteralTest.php b/test/Route/LiteralTest.php index c4813de..274ce23 100644 --- a/test/Route/LiteralTest.php +++ b/test/Route/LiteralTest.php @@ -7,12 +7,12 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; use Zend\Http\Request; -use Zend\Router\Http\Literal; -use Zend\Router\Http\RouteMatch; +use Zend\Router\Route\Literal; +use Zend\Router\Route\RouteMatch; use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; diff --git a/test/Route/MethodTest.php b/test/Route/MethodTest.php index 848abd8..817077d 100644 --- a/test/Route/MethodTest.php +++ b/test/Route/MethodTest.php @@ -7,12 +7,12 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; use Zend\Http\Request; -use Zend\Router\Http\Method as HttpMethod; -use Zend\Router\Http\RouteMatch; +use Zend\Router\Route\Method as HttpMethod; +use Zend\Router\Route\RouteMatch; use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; diff --git a/test/Route/PartTest.php b/test/Route/PartTest.php index 8bab296..ab0ff4e 100644 --- a/test/Route/PartTest.php +++ b/test/Route/PartTest.php @@ -7,18 +7,18 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use ArrayObject; use PHPUnit\Framework\TestCase; use Zend\Http\Request; use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Exception\RuntimeException; -use Zend\Router\Http\Literal; -use Zend\Router\Http\Part; -use Zend\Router\Http\RouteMatch; -use Zend\Router\Http\Segment; -use Zend\Router\Http\Wildcard; +use Zend\Router\Route\Literal; +use Zend\Router\Route\Part; +use Zend\Router\Route\RouteMatch; +use Zend\Router\Route\Segment; +use Zend\Router\Route\Wildcard; use Zend\Router\RouteInvokableFactory; use Zend\Router\RoutePluginManager; use Zend\ServiceManager\ServiceManager; @@ -373,7 +373,7 @@ public function testFactory() 'route_plugins' => 'Missing "route_plugins" in options array' ], [ - 'route' => new \Zend\Router\Http\Literal('/foo'), + 'route' => new Literal('/foo'), 'route_plugins' => self::getRoutePlugins(), ] ); diff --git a/test/Route/RegexTest.php b/test/Route/RegexTest.php index a8a194f..657169d 100644 --- a/test/Route/RegexTest.php +++ b/test/Route/RegexTest.php @@ -7,12 +7,12 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; use Zend\Http\Request; -use Zend\Router\Http\Regex; -use Zend\Router\Http\RouteMatch; +use Zend\Router\Route\Regex; +use Zend\Router\Route\RouteMatch; use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; diff --git a/test/Route/RouteMatchTest.php b/test/Route/RouteMatchTest.php index d20f116..dcc2ca7 100644 --- a/test/Route/RouteMatchTest.php +++ b/test/Route/RouteMatchTest.php @@ -7,10 +7,10 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; -use Zend\Router\Http\RouteMatch; +use Zend\Router\Route\RouteMatch; class RouteMatchTest extends TestCase { diff --git a/test/Route/SchemeTest.php b/test/Route/SchemeTest.php index 88cae63..1d46d5a 100644 --- a/test/Route/SchemeTest.php +++ b/test/Route/SchemeTest.php @@ -7,12 +7,12 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; use Zend\Http\Request; -use Zend\Router\Http\RouteMatch; -use Zend\Router\Http\Scheme; +use Zend\Router\Route\RouteMatch; +use Zend\Router\Route\Scheme; use Zend\Stdlib\Request as BaseRequest; use Zend\Uri\Http as HttpUri; use ZendTest\Router\FactoryTester; diff --git a/test/Route/SegmentTest.php b/test/Route/SegmentTest.php index 06a1a3d..e02aebf 100644 --- a/test/Route/SegmentTest.php +++ b/test/Route/SegmentTest.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace ZendTest\Router\Http; +namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; use Zend\Http\Request; @@ -16,8 +16,8 @@ use Zend\I18n\Translator\Translator; use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Exception\RuntimeException; -use Zend\Router\Http\RouteMatch; -use Zend\Router\Http\Segment; +use Zend\Router\Route\RouteMatch; +use Zend\Router\Route\Segment; use Zend\Stdlib\Request as BaseRequest; use ZendTest\Router\FactoryTester; diff --git a/test/Route/TestAsset/DummyRoute.php b/test/Route/TestAsset/DummyRoute.php index cca7766..706dd37 100644 --- a/test/Route/TestAsset/DummyRoute.php +++ b/test/Route/TestAsset/DummyRoute.php @@ -7,10 +7,10 @@ declare(strict_types=1); -namespace ZendTest\Router\Http\TestAsset; +namespace ZendTest\Router\Route\TestAsset; -use Zend\Router\Http\RouteInterface; -use Zend\Router\Http\RouteMatch; +use Zend\Router\Route\RouteInterface; +use Zend\Router\Route\RouteMatch; use Zend\Stdlib\RequestInterface; /** diff --git a/test/Route/TestAsset/DummyRouteWithParam.php b/test/Route/TestAsset/DummyRouteWithParam.php index 00e6b46..346343e 100644 --- a/test/Route/TestAsset/DummyRouteWithParam.php +++ b/test/Route/TestAsset/DummyRouteWithParam.php @@ -7,9 +7,9 @@ declare(strict_types=1); -namespace ZendTest\Router\Http\TestAsset; +namespace ZendTest\Router\Route\TestAsset; -use Zend\Router\Http\RouteMatch; +use Zend\Router\Route\RouteMatch; use Zend\Stdlib\RequestInterface; /** diff --git a/test/RouterFactoryTest.php b/test/RouterFactoryTest.php index 7574b44..03aca8b 100644 --- a/test/RouterFactoryTest.php +++ b/test/RouterFactoryTest.php @@ -11,7 +11,7 @@ use Interop\Container\ContainerInterface; use PHPUnit\Framework\TestCase; -use Zend\Router\Http\HttpRouterFactory; +use Zend\Router\Route\HttpRouterFactory; use Zend\Router\RoutePluginManager; use Zend\Router\RouterFactory; use Zend\ServiceManager\Config; From 81d45294cbac42d9fbb4e060e0001c0ac31e8a4a Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Mon, 5 Mar 2018 12:07:10 +1000 Subject: [PATCH 17/47] Add PartialRouteTrait for common match() implementation --- src/Route/PartialRouteTrait.php | 50 ++++++++ test/Route/PartialRouteTraitTest.php | 177 +++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 src/Route/PartialRouteTrait.php create mode 100644 test/Route/PartialRouteTraitTest.php diff --git a/src/Route/PartialRouteTrait.php b/src/Route/PartialRouteTrait.php new file mode 100644 index 0000000..ac3d73a --- /dev/null +++ b/src/Route/PartialRouteTrait.php @@ -0,0 +1,50 @@ +partialMatch($request, $pathOffset, $options); + if (! $result->isFullPathMatch($request->getUri())) { + return RouteResult::fromRouteFailure(); + } + if ($result->isSuccess()) { + return RouteResult::fromRouteMatch($result->getMatchedParams(), $result->getMatchedRouteName()); + } + if ($result->isMethodFailure()) { + return RouteResult::fromMethodFailure($result->getAllowedMethods()); + } + // unreachable due to full path match check. It is kept here intentionally + return RouteResult::fromRouteFailure(); + } +} diff --git a/test/Route/PartialRouteTraitTest.php b/test/Route/PartialRouteTraitTest.php new file mode 100644 index 0000000..c8838dd --- /dev/null +++ b/test/Route/PartialRouteTraitTest.php @@ -0,0 +1,177 @@ +request = new ServerRequest([], [], new Uri('/path')); + $this->partial = new class() implements PartialRouteInterface { + use PartialRouteTrait; + + /** + * @var ObjectProphecy + */ + public $prophecy; + + public function partialMatch( + ServerRequestInterface $request, + int $pathOffset = 0, + array $options = [] + ) : PartialRouteResult { + return $this->prophecy->reveal()->partialMatch($request, $pathOffset, $options); + } + + public function getLastAssembledParams() : array + { + return $this->prophecy->reveal()->getLastAssembledParams(); + } + + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface + { + return $this->prophecy->reveal()->assemble($uri, $params, $options); + } + }; + + $this->partial->prophecy = $this->prophesize(PartialRouteInterface::class); + } + + public function testInvokesPartialMatchWithMatchParameters() + { + $partialResult = PartialRouteResult::fromRouteMatch([], 5, 0); + $this->partial->prophecy + ->partialMatch($this->request, 5, ['foo' => 'bar']) + ->shouldBeCalled() + ->willReturn($partialResult); + + $this->partial->match($this->request, 5, ['foo' => 'bar']); + } + + public function testRejectsNegativePathOffset() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $this->partial->match($this->request, -1); + } + + public function testReturnsSuccessOnPartialRouteMatchWithFullPathMatch() + { + $pathLength = strlen($this->request->getUri()->getPath()); + $this->partial->prophecy + ->partialMatch($this->request, 0, []) + ->shouldBeCalled() + ->willReturn(PartialRouteResult::fromRouteMatch([], 0, $pathLength)); + + $result = $this->partial->match($this->request); + $this->assertTrue($result->isSuccess()); + } + + public function testReturnsParametersAndRouteNameFromPartialRouteMatch() + { + $pathLength = strlen($this->request->getUri()->getPath()); + $this->partial->prophecy + ->partialMatch($this->request, 0, []) + ->shouldBeCalled() + ->willReturn(PartialRouteResult::fromRouteMatch(['foo' => 'bar'], 0, $pathLength, 'routename')); + + $result = $this->partial->match($this->request); + $this->assertEquals(['foo' => 'bar'], $result->getMatchedParams()); + $this->assertEquals('routename', $result->getMatchedRouteName()); + } + + public function testReturnsFailureOnPartialRouteMatchWithPartialPathMatch() + { + $pathLength = strlen($this->request->getUri()->getPath()); + $this->partial->prophecy + ->partialMatch($this->request, 0, []) + ->shouldBeCalled() + ->willReturn(PartialRouteResult::fromRouteMatch([], 0, $pathLength - 1)); + + $result = $this->partial->match($this->request); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); + } + + public function testReturnsFailureOnPartialFailure() + { + $this->partial->prophecy + ->partialMatch($this->request, 0, []) + ->shouldBeCalled() + ->willReturn(PartialRouteResult::fromRouteFailure()); + $result = $this->partial->match($this->request); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); + } + + public function testReturnsFailureOnPartialFailureWithFullPathMatch() + { + $pathLength = strlen($this->request->getUri()->getPath()); + $this->partial->prophecy + ->partialMatch($this->request, $pathLength, []) + ->shouldBeCalled() + ->willReturn(PartialRouteResult::fromRouteFailure()); + $result = $this->partial->match($this->request, $pathLength); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); + } + + public function testReturnsMethodFailureOnPartialMethodFailureWithFullPathMatch() + { + $pathLength = strlen($this->request->getUri()->getPath()); + $this->partial->prophecy + ->partialMatch($this->request, 0, []) + ->shouldBeCalled() + ->willReturn(PartialRouteResult::fromMethodFailure(['GET', 'POST'], 0, $pathLength)); + + $result = $this->partial->match($this->request); + $this->assertTrue($result->isFailure()); + $this->assertTrue($result->isMethodFailure()); + $this->assertEquals(['GET', 'POST'], $result->getAllowedMethods()); + } + + public function testReturnsFailureOnPartialMethodFailure() + { + $pathLength = strlen($this->request->getUri()->getPath()); + $this->partial->prophecy + ->partialMatch($this->request, 0, []) + ->shouldBeCalled() + ->willReturn(PartialRouteResult::fromMethodFailure(['GET', 'POST'], 0, $pathLength - 1)); + $result = $this->partial->match($this->request, 0, []); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); + } +} From 8d80862e4f34ea23405b03cefa4b6d288fcad426 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Tue, 6 Mar 2018 13:30:26 +1000 Subject: [PATCH 18/47] Add helper class and traits for route testing - RoutTestDefinition is a value object to programatically set used parameters and expected results for use by data providers defined in traits. - RouteTestTrait provides route matching and uri assembling tests along with data providers for them - PartialRouteTestTrait provides partial route matching test with data provider for it --- test/Route/PartialRouteTestTrait.php | 111 ++++++++++ test/Route/RouteTestTrait.php | 129 ++++++++++++ test/Route/TestAsset/RouteTestDefinition.php | 211 +++++++++++++++++++ 3 files changed, 451 insertions(+) create mode 100644 test/Route/PartialRouteTestTrait.php create mode 100644 test/Route/RouteTestTrait.php create mode 100644 test/Route/TestAsset/RouteTestDefinition.php diff --git a/test/Route/PartialRouteTestTrait.php b/test/Route/PartialRouteTestTrait.php new file mode 100644 index 0000000..a9132d5 --- /dev/null +++ b/test/Route/PartialRouteTestTrait.php @@ -0,0 +1,111 @@ +getRouteTestDefinitions(); + foreach ($definitions as $description => $definition) { + /** + * @var RouteTestDefinition $definition + */ + $data[$description] = [ + $definition->getRoute(), + $definition->getRequestToMatch(), + $definition->getPathOffset(), + $definition->getMatchOptions(), + $definition->getExpectedPartialMatchResult(), + ]; + } + return $data; + } + + /** + * We use callback instead of route instance so that we can get coverage + * for all route configuration combinations. + * + * @dataProvider partialRouteMatchingProvider + */ + public function testPartialMatching( + PartialRouteInterface $route, + Request $request, + int $pathOffset, + array $matchOptions, + PartialRouteResult $expectedResult + ) { + $result = $route->partialMatch($request, $pathOffset, $matchOptions); + + if ($expectedResult->isSuccess()) { + $this->assertTrue($result->isSuccess(), 'Expected successful routing'); + $expectedParams = $expectedResult->getMatchedParams(); + ksort($expectedParams); + $actualParams = $result->getMatchedParams(); + ksort($expectedParams); + $this->assertEquals($expectedParams, $actualParams, 'Matched parameters do not meet test expectation'); + + $this->assertSame( + $expectedResult->getMatchedRouteName(), + $result->getMatchedRouteName(), + 'Expected matched route name do not meet test expectation' + ); + $this->assertEquals( + $expectedResult->getMatchedPathLength(), + $result->getMatchedPathLength(), + 'Expected path match length does not meet test expectation' + ); + $this->assertEquals( + $expectedResult->getUsedPathOffset(), + $result->getUsedPathOffset(), + 'Expected path offset does not meet test expectation' + ); + } + if ($expectedResult->isFailure()) { + $this->assertTrue($result->isFailure(), 'Failed routing is expected'); + } + if ($expectedResult->isMethodFailure()) { + $this->assertTrue($result->isMethodFailure(), 'Http method routing failure is expected'); + + $expectedMethods = $expectedResult->getAllowedMethods(); + sort($expectedMethods); + $actualMethods = $result->getAllowedMethods(); + sort($actualMethods); + + $this->assertEquals($expectedMethods, $actualMethods, 'Allowed http methods do not match expectation'); + + $this->assertEquals( + $expectedResult->getMatchedPathLength(), + $result->getMatchedPathLength(), + 'Expected path match length does not meet test expectation' + ); + $this->assertEquals( + $expectedResult->getUsedPathOffset(), + $result->getUsedPathOffset(), + 'Expected path offset does not meet test expectation' + ); + } + } +} diff --git a/test/Route/RouteTestTrait.php b/test/Route/RouteTestTrait.php new file mode 100644 index 0000000..07fb625 --- /dev/null +++ b/test/Route/RouteTestTrait.php @@ -0,0 +1,129 @@ +getRouteTestDefinitions(); + foreach ($definitions as $description => $definition) { + /** + * @var RouteTestDefinition $definition + */ + yield $description => [ + $definition->getRoute(), + $definition->getRequestToMatch(), + $definition->getPathOffset(), + $definition->getMatchOptions(), + $definition->getExpectedMatchResult(), + ]; + } + } + + /** + * We use callback instead of route instance so that we can get coverage + * for all route configuration combinations. + * + * @dataProvider routeMatchingProvider + */ + public function testMatching( + RouteInterface $route, + Request $request, + int $pathOffset, + array $matchOptions, + RouteResult $expectedResult + ) { + $result = $route->match($request, $pathOffset, $matchOptions); + + if ($expectedResult->isSuccess()) { + $this->assertTrue($result->isSuccess(), 'Expected successful routing'); + $expectedParams = $expectedResult->getMatchedParams(); + ksort($expectedParams); + $actualParams = $result->getMatchedParams(); + ksort($expectedParams); + $this->assertEquals($expectedParams, $actualParams, 'Matched parameters do not meet test expectation'); + + $this->assertSame( + $expectedResult->getMatchedRouteName(), + $result->getMatchedRouteName(), + 'Expected matched route name do not meet test expectation' + ); + } + if ($expectedResult->isFailure()) { + $this->assertTrue($result->isFailure(), 'Failed routing is expected'); + } + if ($expectedResult->isMethodFailure()) { + $this->assertTrue($result->isMethodFailure(), 'Http method routing failure is expected'); + + $expectedMethods = $expectedResult->getAllowedMethods(); + sort($expectedMethods); + $actualMethods = $result->getAllowedMethods(); + sort($actualMethods); + + $this->assertEquals($expectedMethods, $actualMethods, 'Allowed http methods do not match expectation'); + } + } + + /** + * @uses self::getRouteTestDefinitions() provided definitions to prepare and + * provide data for route assembling uri test + */ + public function routeUriAssemblingProvider() : iterable + { + $definitions = $this->getRouteTestDefinitions(); + foreach ($definitions as $description => $definition) { + /** + * @var RouteTestDefinition $definition + */ + $assembleResult = $definition->getExpectedAssembleResult(); + if (null === $assembleResult) { + continue; + } + yield $description => [ + $definition->getRoute(), + $definition->getUriForAssemble(), + $definition->getParamsForAssemble(), + $definition->getOptionsForAssemble(), + $assembleResult, + ]; + } + } + + /** + * @dataProvider routeUriAssemblingProvider + */ + public function testAssembling( + RouteInterface $route, + UriInterface $uriForAssemble, + array $params, + array $options, + UriInterface $expectedUri + ) { + $uri = $route->assemble($uriForAssemble, $params, $options); + + $this->assertEquals($expectedUri->__toString(), $uri->__toString()); + } +} diff --git a/test/Route/TestAsset/RouteTestDefinition.php b/test/Route/TestAsset/RouteTestDefinition.php new file mode 100644 index 0000000..cff6366 --- /dev/null +++ b/test/Route/TestAsset/RouteTestDefinition.php @@ -0,0 +1,211 @@ +route = $route; + if ($requestOrUriToMatch instanceof ServerRequestInterface) { + $this->matchRequest = $requestOrUriToMatch; + } elseif ($requestOrUriToMatch instanceof UriInterface) { + $this->matchRequest = new ServerRequest([], [], $requestOrUriToMatch, 'GET', 'php://memory'); + } else { + throw new Exception('Must provide server request or uri interface to use for matching'); + } + } + + public function getRoute() : RouteInterface + { + return $this->route; + } + + public function getRequestToMatch() : ServerRequestInterface + { + return $this->matchRequest; + } + + public function expectMatchResult(RouteResult $result) : self + { + $this->matchResult = $result; + return $this; + } + + public function getExpectedMatchResult() : RouteResult + { + if (! $this->matchResult) { + throw new Exception( + 'Expected match result is not provided. Set it with RouteTestDefinition::expectMatchResult()' + ); + } + return $this->matchResult; + } + + + public function expectPartialMatchResult(PartialRouteResult $result) : self + { + if (! $this->route instanceof PartialRouteInterface) { + throw new Exception('Only partial route can match partially'); + } + + $this->partialMatchResult = $result; + return $this; + } + + public function getExpectedPartialMatchResult() : PartialRouteResult + { + if (! $this->route instanceof PartialRouteInterface) { + throw new Exception('No expected partial match result. Only partial route can match partially'); + } + if (! $this->partialMatchResult) { + throw new Exception( + 'Expected partial match result is not provided. Set it with' + . ' RouteTestDefinition::expectPartialMatchResult()' + ); + } + return $this->partialMatchResult; + } + + public function usePathOffset(int $pathOffset) : self + { + $this->pathOffset = $pathOffset; + return $this; + } + + public function getPathOffset() : int + { + return $this->pathOffset; + } + + public function useMatchOptions(array $options) : self + { + $this->matchOptions = $options; + return $this; + } + + public function getMatchOptions() : array + { + return $this->matchOptions; + } + + public function shouldAssembleAndExpectResult(UriInterface $uri) : self + { + $this->assembleResult = $uri; + return $this; + } + + public function shouldAssembleAndExpectResultSameAsUriForMatching() : self + { + $this->shouldAssembleAndExpectResult($this->getRequestToMatch()->getUri()); + return $this; + } + + public function getExpectedAssembleResult() : ?UriInterface + { + return $this->assembleResult; + } + + public function useUriForAssemble(UriInterface $uri) : self + { + $this->assembleWithUri = $uri; + return $this; + } + + public function getUriForAssemble() : UriInterface + { + return $this->assembleWithUri ?? new Uri(); + } + + public function useParamsForAssemble(array $assembleParams) : self + { + $this->assembleParams = $assembleParams; + return $this; + } + + public function getParamsForAssemble() : array + { + return $this->assembleParams; + } + + public function useOptionsForAssemble(array $assembleOptions) : self + { + $this->assembleOptions = $assembleOptions; + return $this; + } + + public function getOptionsForAssemble() : array + { + return $this->assembleOptions; + } +} From b83e9fc3d344d1af6ae4c49268e2b8c0a7067a5f Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Tue, 6 Mar 2018 14:00:08 +1000 Subject: [PATCH 19/47] Refactor Literal route for PSR-7 --- src/Route/Literal.php | 114 ++++++++++------------ test/FactoryTester.php | 8 -- test/Route/LiteralTest.php | 195 ++++++++++++++++++------------------- 3 files changed, 144 insertions(+), 173 deletions(-) diff --git a/src/Route/Literal.php b/src/Route/Literal.php index 67f3094..5d557a7 100644 --- a/src/Route/Literal.php +++ b/src/Route/Literal.php @@ -1,7 +1,7 @@ route = $route; + if (empty($path)) { + throw new InvalidArgumentException('Literal uri path part cannot be empty'); + } + $this->path = $path; $this->defaults = $defaults; } /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $options - * @return Literal - * @throws Exception\InvalidArgumentException + * @todo provide factory for route plugin manager + * @throws InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []) : self { - if ($options instanceof Traversable) { + if (! is_array($options)) { $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'); + throw new InvalidArgumentException('Missing "route" in options array'); } if (! isset($options['defaults'])) { @@ -76,60 +77,45 @@ public static function factory($options = []) } /** - * match(): defined by RouteInterface interface. + * Attempt to match ServerRequestInterface by checking for literal + * path segment at offset position. * - * @see \Zend\Router\RouteInterface::match() - * @param Request $request - * @param integer|null $pathOffset - * @return RouteMatch|null + * @throws InvalidArgumentException */ - public function match(Request $request, $pathOffset = null) + public function partialMatch(Request $request, int $pathOffset = 0, array $options = []) : PartialRouteResult { - if (! method_exists($request, 'getUri')) { - return; - } - - $uri = $request->getUri(); - $path = $uri->getPath(); - - if ($pathOffset !== null) { - if ($pathOffset >= 0 && strlen((string) $path) >= $pathOffset && ! empty($this->route)) { - if (strpos($path, $this->route, $pathOffset) === $pathOffset) { - return new RouteMatch($this->defaults, strlen($this->route)); - } - } - - return; + if ($pathOffset < 0) { + throw new InvalidArgumentException('Path offset cannot be negative'); } + $path = $request->getUri()->getPath(); - if ($path === $this->route) { - return new RouteMatch($this->defaults, strlen($this->route)); + if (strpos($path, $this->path, $pathOffset) === $pathOffset) { + return PartialRouteResult::fromRouteMatch($this->defaults, $pathOffset, strlen($this->path)); } - - return; + return PartialRouteResult::fromRouteFailure(); } /** - * assemble(): Defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed + * Assemble url by appending literal path part */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { - return $this->route; + return $uri->withPath($uri->getPath() . $this->path); } /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see RouteInterface::getAssembledParams - * @return array + * Literal routes are not using parameters to assemble uri */ - public function getAssembledParams() + public function getLastAssembledParams() : array { return []; } + + /** + * @deprecated + */ + public function getAssembledParams() : array + { + return $this->getLastAssembledParams(); + } } diff --git a/test/FactoryTester.php b/test/FactoryTester.php index dc00eb4..dc2630b 100644 --- a/test/FactoryTester.php +++ b/test/FactoryTester.php @@ -42,14 +42,6 @@ public function __construct(TestCase $testCase) */ public function testFactory($classname, array $requiredOptions, array $options) { - // Test that the factory does not allow a scalar option. - try { - $classname::factory(0); - $this->testCase->fail('An expected exception was not thrown'); - } catch (\Zend\Router\Exception\InvalidArgumentException $e) { - $this->testCase->assertContains('factory expects an array or Traversable set of options', $e->getMessage()); - } - // Test required options. foreach ($requiredOptions as $option => $exceptionMessage) { $testOptions = $options; diff --git a/test/Route/LiteralTest.php b/test/Route/LiteralTest.php index 274ce23..3f7ee53 100644 --- a/test/Route/LiteralTest.php +++ b/test/Route/LiteralTest.php @@ -1,7 +1,7 @@ [ - new Literal('/foo'), - '/foo', - null, - true - ], - 'no-match-without-leading-slash' => [ - new Literal('foo'), - '/foo', - null, - false - ], - 'no-match-with-trailing-slash' => [ - new Literal('/foo'), - '/foo/', - null, - false - ], - 'offset-skips-beginning' => [ - new Literal('foo'), - '/foo', - 1, - true - ], - 'offset-enables-partial-matching' => [ - new Literal('/foo'), - '/foo/bar', - 0, - true - ], - ]; - } - - /** - * @dataProvider routeProvider - * @param Literal $route - * @param string $path - * @param int $offset - * @param bool $shouldMatch - */ - public function testMatching(Literal $route, $path, $offset, $shouldMatch) - { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset); - - if (! $shouldMatch) { - $this->assertNull($match); - } else { - $this->assertInstanceOf(RouteMatch::class, $match); - - if ($offset === null) { - $this->assertEquals(strlen($path), $match->getLength()); - } - } - } + use PartialRouteTestTrait; - /** - * @dataProvider routeProvider - * @param Literal $route - * @param string $path - * @param int $offset - * @param bool $shouldMatch - */ - public function testAssembling(Literal $route, $path, $offset, $shouldMatch) + public function getRouteTestDefinitions() : iterable { - if (! $shouldMatch) { - // Data which will not match are not tested for assembling. - return; - } - - $result = $route->assemble(); - - if ($offset !== null) { - $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)); + yield 'simple match' => (new RouteTestDefinition( + new Literal('/foo'), + new Uri('/foo') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching(); + + yield 'no match without leading slash' => (new RouteTestDefinition( + new Literal('foo'), + new Uri('/foo') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + yield 'only partial match with trailing slash' => (new RouteTestDefinition( + new Literal('/foo'), + new Uri('/foo/') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 4) + ); + yield 'offset skips beginning' => (new RouteTestDefinition( + new Literal('foo'), + new Uri('/foo') + )) + ->usePathOffset(1) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 1, 3) + ); + yield 'offset does not prevent partial match' => (new RouteTestDefinition( + new Literal('foo'), + new Uri('/foo/bar') + )) + ->usePathOffset(1) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 1, 3) + ); + yield 'assemble appends to path present in provided uri' => (new RouteTestDefinition( + new Literal('/foo'), + new Uri('/foo') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 4) + ) + ->useUriForAssemble(new Uri('/bar')) + ->shouldAssembleAndExpectResult(new Uri('/bar/foo')); } public function testGetAssembledParams() { + $uri = new Uri(); $route = new Literal('/foo'); - $route->assemble(['foo' => 'bar']); + $route->assemble($uri, ['foo' => 'bar']); - $this->assertEquals([], $route->getAssembledParams()); + $this->assertEquals([], $route->getLastAssembledParams()); + $this->assertEquals($route->getLastAssembledParams(), $route->getAssembledParams()); } public function testFactory() @@ -126,21 +113,27 @@ public function testFactory() $tester->testFactory( Literal::class, [ - 'route' => 'Missing "route" in options array' + 'route' => 'Missing "route" in options array', ], [ - 'route' => '/foo' + 'route' => '/foo', ] ); } - /** - * @group ZF2-436 - */ public function testEmptyLiteral() { - $request = new Request(); - $route = new Literal(''); - $this->assertNull($route->match($request, 0)); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Literal uri path part cannot be empty'); + new Literal(''); + } + + public function testRejectsNegativePathOffset() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $request = $this->prophesize(ServerRequestInterface::class); + $route = new Literal('/foo'); + $route->partialMatch($request->reveal(), -1); } } From 0f7b74ff89a395adeff2296cda86f48c18990246 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Tue, 6 Mar 2018 19:06:32 +1000 Subject: [PATCH 20/47] Refactor Hostname route for PSR-7 --- src/Route/Hostname.php | 148 ++++----- test/Route/HostnameTest.php | 604 ++++++++++++++++++++++++------------ 2 files changed, 475 insertions(+), 277 deletions(-) diff --git a/src/Route/Hostname.php b/src/Route/Hostname.php index 2f4c880..4557f3e 100644 --- a/src/Route/Hostname.php +++ b/src/Route/Hostname.php @@ -9,16 +9,29 @@ namespace Zend\Router\Route; -use Traversable; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; use Zend\Router\Exception; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\PartialRouteInterface; +use Zend\Router\PartialRouteResult; use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; + +use function array_merge; +use function count; +use function is_array; +use function preg_match; +use function preg_quote; +use function sprintf; +use function strlen; /** * Hostname route. */ -class Hostname implements RouteInterface +class Hostname implements PartialRouteInterface { + use PartialRouteTrait; + /** * Parts of the route. * @@ -56,39 +69,25 @@ class Hostname implements RouteInterface /** * Create a new hostname route. - * - * @param string $route - * @param array $constraints - * @param array $defaults */ - 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); - $this->regex = $this->buildRegex($this->parts, $constraints); + $this->parts = $this->parseRouteDefinition($route); + $this->regex = $this->buildRegex($this->parts, $constraints); } /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $options - * @return Hostname - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []) : self { - if ($options instanceof Traversable) { + if (! is_array($options)) { $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'); + throw new InvalidArgumentException('Missing "route" in options array'); } if (! isset($options['constraints'])) { @@ -105,17 +104,15 @@ public static function factory($options = []) /** * 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); - $parts = []; + $length = strlen($def); + $parts = []; $levelParts = [&$parts]; - $level = 0; + $level = 0; while ($currentPos < $length) { if (! preg_match('(\G(?P[a-z0-9-.]*)(?P[:{\[\]]|$))', $def, $matches, 0, $currentPos)) { @@ -142,7 +139,7 @@ protected function parseRouteDefinition($def) $levelParts[$level][] = [ 'parameter', $matches['name'], - isset($matches['delimiters']) ? $matches['delimiters'] : null + isset($matches['delimiters']) ? $matches['delimiters'] : null, ]; $currentPos += strlen($matches[0]); @@ -172,14 +169,8 @@ protected function parseRouteDefinition($def) /** * Build the matching regex from parsed parts. - * - * @param array $parts - * @param array $constraints - * @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 = ''; @@ -215,17 +206,12 @@ protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1 /** * Build host. * - * @param array $parts - * @param array $mergedParams - * @param bool $isOptional - * @return string - * @throws Exception\RuntimeException - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException */ - protected function buildHost(array $parts, array $mergedParams, $isOptional) + protected function buildHost(array $parts, array $mergedParams, bool $isOptional) : string { - $host = ''; - $skip = true; + $host = ''; + $skip = true; $skippable = false; foreach ($parts as $part) { @@ -239,7 +225,7 @@ protected function buildHost(array $parts, array $mergedParams, $isOptional) if (! isset($mergedParams[$part[1]])) { if (! $isOptional) { - throw new Exception\InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1])); + throw new InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1])); } return ''; @@ -256,12 +242,12 @@ protected function buildHost(array $parts, array $mergedParams, $isOptional) break; case 'optional': - $skippable = true; + $skippable = true; $optionalPart = $this->buildHost($part[1], $mergedParams, true); if ($optionalPart !== '') { $host .= $optionalPart; - $skip = false; + $skip = false; } break; } @@ -275,25 +261,20 @@ protected function buildHost(array $parts, array $mergedParams, $isOptional) } /** - * match(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::match() - * @param Request $request - * @return RouteMatch|null + * @throws InvalidArgumentException */ - public function match(Request $request) + public function partialMatch(Request $request, int $pathOffset = 0, array $options = []) : PartialRouteResult { - if (! method_exists($request, 'getUri')) { - return; + if ($pathOffset < 0) { + throw new InvalidArgumentException('Path offset cannot be negative'); } - - $uri = $request->getUri(); + $uri = $request->getUri(); $host = $uri->getHost(); $result = preg_match('(^' . $this->regex . '$)', $host, $matches); if (! $result) { - return; + return PartialRouteResult::fromRouteFailure(); } $params = []; @@ -304,43 +285,36 @@ public function match(Request $request) } } - return new RouteMatch(array_merge($this->defaults, $params)); + return PartialRouteResult::fromRouteMatch(array_merge($this->defaults, $params), $pathOffset, 0); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { $this->assembledParams = []; - if (isset($options['uri'])) { - $host = $this->buildHost( - $this->parts, - array_merge($this->defaults, $params), - false - ); - - $options['uri']->setHost($host); - } - - // A hostname does not contribute to the path, thus nothing is returned. - return ''; + return $uri->withHost($this->buildHost( + $this->parts, + array_merge($this->defaults, $params), + false + )); } /** - * getAssembledParams(): defined by RouteInterface interface. + * Get parameters used to assemble uri on the last assemble invocation. + * Used during uri assembling by Part and Chain routes * - * @see RouteInterface::getAssembledParams - * @return array + * @internal */ - public function getAssembledParams() + public function getLastAssembledParams() : array { return $this->assembledParams; } + + /** + * @deprecated + */ + public function getAssembledParams() : array + { + return $this->getLastAssembledParams(); + } } diff --git a/test/Route/HostnameTest.php b/test/Route/HostnameTest.php index e56214c..0351b7d 100644 --- a/test/Route/HostnameTest.php +++ b/test/Route/HostnameTest.php @@ -10,228 +10,446 @@ namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; -use Zend\Http\Request; +use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\Uri; use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Exception\RuntimeException; +use Zend\Router\PartialRouteResult; use Zend\Router\Route\Hostname; -use Zend\Router\Route\RouteMatch; -use Zend\Stdlib\Request as BaseRequest; -use Zend\Uri\Http as HttpUri; +use Zend\Router\RouteResult; use ZendTest\Router\FactoryTester; +use ZendTest\Router\Route\TestAsset\RouteTestDefinition; /** * @covers \Zend\Router\Route\Hostname */ class HostnameTest extends TestCase { - public static function routeProvider() - { - return [ - 'simple-match' => [ - new Hostname(':foo.example.com'), - 'bar.example.com', - ['foo' => 'bar'] - ], - 'no-match-on-different-hostname' => [ - new Hostname('foo.example.com'), - 'bar.example.com', - null - ], - 'no-match-with-different-number-of-parts' => [ - new Hostname('foo.example.com'), - 'example.com', - null - ], - 'no-match-with-different-number-of-parts-2' => [ - new Hostname('example.com'), - 'foo.example.com', - null - ], - 'match-overrides-default' => [ - new Hostname(':foo.example.com', [], ['foo' => 'baz']), - 'bat.example.com', - ['foo' => 'bat'] - ], - 'constraints-prevent-match' => [ - new Hostname(':foo.example.com', ['foo' => '\d+']), - 'bar.example.com', - null - ], - 'constraints-allow-match' => [ - new Hostname(':foo.example.com', ['foo' => '\d+']), - '123.example.com', - ['foo' => '123'] - ], - 'constraints-allow-match-2' => [ - new Hostname( - 'www.:domain.com', - ['domain' => '(mydomain|myaltdomain1|myaltdomain2)'], - ['domain' => 'mydomain'] - ), - 'www.mydomain.com', - ['domain' => 'mydomain'] - ], - 'optional-subdomain' => [ - new Hostname('[:foo.]example.com'), - 'bar.example.com', - ['foo' => 'bar'], - ], - 'two-optional-subdomain' => [ - new Hostname('[:foo.][:bar.]example.com'), - 'baz.bat.example.com', - ['foo' => 'baz', 'bar' => 'bat'], - ], - 'missing-optional-subdomain' => [ - new Hostname('[:foo.]example.com'), - 'example.com', - ['foo' => null], - ], - 'one-of-two-missing-optional-subdomain' => [ - new Hostname('[:foo.][:bar.]example.com'), - 'bat.example.com', - ['foo' => null, 'foo' => 'bat'], - ], - 'two-missing-optional-subdomain' => [ - new Hostname('[:foo.][:bar.]example.com'), - 'example.com', - ['foo' => null, 'bar' => null], - ], - 'two-optional-subdomain-nested' => [ - new Hostname('[[:foo.]:bar.]example.com'), - 'baz.bat.example.com', - ['foo' => 'baz', 'bar' => 'bat'], - ], - 'one-of-two-missing-optional-subdomain-nested' => [ - new Hostname('[[:foo.]:bar.]example.com'), - 'bat.example.com', - ['foo' => null, 'bar' => 'bat'], - ], - 'two-missing-optional-subdomain-nested' => [ - new Hostname('[[:foo.]:bar.]example.com'), - 'example.com', - ['foo' => null, 'bar' => null], - ], - 'no-match-on-different-hostname-and-optional-subdomain' => [ - new Hostname('[:foo.]example.com'), - 'bar.test.com', - null, - ], - 'no-match-with-different-number-of-parts-and-optional-subdomain' => [ - new Hostname('[:foo.]example.com'), - 'bar.baz.example.com', - null, - ], - 'match-overrides-default-optional-subdomain' => [ - new Hostname('[:foo.]:bar.example.com', [], ['bar' => 'baz']), - 'bat.qux.example.com', - ['foo' => 'bat', 'bar' => 'qux'], - ], - 'constraints-prevent-match-optional-subdomain' => [ - new Hostname('[:foo.]example.com', ['foo' => '\d+']), - 'bar.example.com', - null, - ], - 'constraints-allow-match-optional-subdomain' => [ - new Hostname('[:foo.]example.com', ['foo' => '\d+']), - '123.example.com', - ['foo' => '123'], - ], - 'middle-subdomain-optional' => [ - new Hostname(':foo.[:bar.]example.com'), - 'baz.bat.example.com', - ['foo' => 'baz', 'bar' => 'bat'], - ], - 'missing-middle-subdomain-optional' => [ - new Hostname(':foo.[:bar.]example.com'), - 'baz.example.com', - ['foo' => 'baz'], - ], - 'non-standard-delimeter' => [ - new Hostname('user-:username.example.com'), - 'user-jdoe.example.com', - ['username' => 'jdoe'], - ], - 'non-standard-delimeter-optional' => [ - new Hostname(':page{-}[-:username].example.com'), - 'article-jdoe.example.com', - ['page' => 'article', 'username' => 'jdoe'], - ], - 'missing-non-standard-delimeter-optional' => [ - new Hostname(':page{-}[-:username].example.com'), - 'article.example.com', - ['page' => 'article'], - ], - ]; - } + use PartialRouteTestTrait; + use RouteTestTrait; /** - * @dataProvider routeProvider - * @param Hostname $route - * @param string $hostname - * @param array $params + * Provides route test definitions. As a data provider it does not + * generate coverage report for route instantiation and configuration logic + * triggered on newing. */ - public function testMatching(Hostname $route, $hostname, array $params = null) + public function getRouteTestDefinitions() : iterable { - $request = new Request(); - $request->setUri('http://' . $hostname . '/'); - $match = $route->match($request); - - if ($params === null) { - $this->assertNull($match); - } else { - $this->assertInstanceOf(RouteMatch::class, $match); - - foreach ($params as $key => $value) { - $this->assertEquals($value, $match->getParam($key)); - } - } + $params = ['foo' => 'bar']; + yield 'simple match' => (new RouteTestDefinition( + new Hostname(':foo.example.com'), + (new Uri())->withHost('bar.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'no match on different hostname' => (new RouteTestDefinition( + new Hostname('foo.example.com'), + (new Uri())->withHost('bar.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + yield 'no match with different number of parts' => (new RouteTestDefinition( + new Hostname('foo.example.com'), + (new Uri())->withHost('example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + yield 'no match with different number of parts 2' => (new RouteTestDefinition( + new Hostname('example.com'), + (new Uri())->withHost('foo.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + $params = ['foo' => 'bat']; + yield 'match overrides default' => (new RouteTestDefinition( + new Hostname(':foo.example.com', [], ['foo' => 'baz']), + (new Uri())->withHost('bat.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'constraints prevent match' => (new RouteTestDefinition( + new Hostname(':foo.example.com', ['foo' => '\d+']), + (new Uri())->withHost('bar.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + $params = ['foo' => '123']; + yield 'constraints allow match' => (new RouteTestDefinition( + new Hostname(':foo.example.com', ['foo' => '\d+']), + (new Uri())->withHost('123.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['domain' => 'mydomain']; + yield 'constraints allow match 2' => (new RouteTestDefinition( + new Hostname( + 'www.:domain.com', + ['domain' => '(mydomain|myaltdomain1|myaltdomain2)'], + ['domain' => 'mydomain'] + ), + (new Uri())->withHost('www.mydomain.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar']; + yield 'optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.]example.com'), + (new Uri())->withHost('bar.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'baz', 'bar' => 'bat']; + yield 'two optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.][:bar.]example.com'), + (new Uri())->withHost('baz.bat.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'missing optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.]example.com'), + (new Uri())->withHost('example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble([]); + + yield 'Assemble with optional parameter equal to null' => (new RouteTestDefinition( + new Hostname('[:foo.]example.com'), + (new Uri())->withHost('example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble(['foo' => null]); + + /** + * @todo investigate if this can be fixed or should be documented as a quirk + * + * There is a workaround, [[:foo.]:bar.] removes ambiguity. Fix could + * be by emulating such nesting for same level optional parts, but it + * might break other use cases + */ + $params = ['bar' => 'bat']; + yield 'optional parameters evaluated right to left' => (new RouteTestDefinition( + new Hostname('[:foo.][:bar.]example.com'), + (new Uri())->withHost('bat.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'two missing optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.][:bar.]example.com'), + (new Uri())->withHost('example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching(); + + $params = ['foo' => 'baz', 'bar' => 'bat']; + yield 'two optional subdomain nested' => (new RouteTestDefinition( + new Hostname('[[:foo.]:bar.]example.com'), + (new Uri())->withHost('baz.bat.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['bar' => 'bat']; + yield 'one of two missing optional subdomain nested' => (new RouteTestDefinition( + new Hostname('[[:foo.]:bar.]example.com'), + (new Uri())->withHost('bat.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'two missing optional subdomain nested' => (new RouteTestDefinition( + new Hostname('[[:foo.]:bar.]example.com'), + (new Uri())->withHost('example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching(); + + yield 'no match on different hostname and optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.]example.com'), + (new Uri())->withHost('bar.test.com') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + yield 'no match with different number of parts and optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.]example.com'), + (new Uri())->withHost('bar.baz.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + $params = ['foo' => 'bat', 'bar' => 'qux']; + yield 'match overrides default optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.]:bar.example.com', [], ['bar' => 'baz']), + (new Uri())->withHost('bat.qux.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'constraints prevent match optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.]example.com', ['foo' => '\d+']), + (new Uri())->withHost('bar.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + $params = ['foo' => '123']; + yield 'constraints allow match optional subdomain' => (new RouteTestDefinition( + new Hostname('[:foo.]example.com', ['foo' => '\d+']), + (new Uri())->withHost('123.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'baz', 'bar' => 'bat']; + yield 'middle subdomain optional' => (new RouteTestDefinition( + new Hostname(':foo.[:bar.]example.com'), + (new Uri())->withHost('baz.bat.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + // @TODO Revisit this behavior. It looks error prone and may be dangerous + $params = ['foo' => 'baz']; + yield 'missing middle subdomain optional' => (new RouteTestDefinition( + new Hostname(':foo.[:bar.]example.com'), + (new Uri())->withHost('baz.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['username' => 'jdoe']; + yield 'non standard delimiter' => (new RouteTestDefinition( + new Hostname('user-:username.example.com'), + (new Uri())->withHost('user-jdoe.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['page' => 'article', 'username' => 'jdoe']; + yield 'non standard delimiter optional' => (new RouteTestDefinition( + new Hostname(':page{-}[-:username].example.com'), + (new Uri())->withHost('article-jdoe.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['page' => 'article']; + yield 'missing non standard delimiter optional' => (new RouteTestDefinition( + new Hostname(':page{-}[-:username].example.com'), + (new Uri())->withHost('article.example.com') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 0) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); } - /** - * @dataProvider routeProvider - * @param Hostname $route - * @param string $hostname - * @param array $params - */ - public function testAssembling(Hostname $route, $hostname, array $params = null) + public function testOnAssembleLeftMostOptionalPartWithProvidedParameterMakesEverythingToTheRightRequired() { - if ($params === null) { - // Data which will not match are not tested for assembling. - return; - } + // @TODO further investigation needed. See todo for 'optional parameters evaluated right to left' + $this->markTestIncomplete(); + $route = new Hostname('[:foo][:bar].example.com'); + $uri = new Uri(); - $uri = new HttpUri(); - $path = $route->assemble($params, ['uri' => $uri]); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Missing parameter "bar"'); + $route->assemble($uri, ['foo' => 'baz']); + } - $this->assertEquals('', $path); - $this->assertEquals($hostname, $uri->getHost()); + public function testHostnameDefinitionWithEmptyParameterNameIsThrowing() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('empty parameter name'); + new Hostname(':.example.com'); } - public function testNoMatchWithoutUriMethod() + public function testHostnameDefinitionWithUnpairedBracketsIsThrowing() { - $route = new Hostname('example.com'); - $request = new BaseRequest(); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Found unbalanced brackets'); + new Hostname('[:foo[:bar].example.com'); + } - $this->assertNull($route->match($request)); + public function testHostnameDefinitionWithClosingBracketAndMissingOpening() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Found closing bracket without matching opening bracket'); + new Hostname(':foo[:bar]].example.com'); } public function testAssemblingWithMissingParameter() { $route = new Hostname(':foo.example.com'); - $uri = new HttpUri(); + $uri = new Uri(); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing parameter "foo"'); - $route->assemble([], ['uri' => $uri]); + $route->assemble($uri, []); } public function testGetAssembledParams() { $route = new Hostname(':foo.example.com'); - $uri = new HttpUri(); - $route->assemble(['foo' => 'bar', 'baz' => 'bat'], ['uri' => $uri]); + $uri = new Uri(); + $route->assemble($uri, ['foo' => 'bar', 'baz' => 'bat']); - $this->assertEquals(['foo'], $route->getAssembledParams()); + $this->assertEquals(['foo'], $route->getLastAssembledParams()); + $this->assertEquals($route->getLastAssembledParams(), $route->getAssembledParams()); } public function testFactory() @@ -240,20 +458,26 @@ public function testFactory() $tester->testFactory( Hostname::class, [ - 'route' => 'Missing "route" in options array' + 'route' => 'Missing "route" in options array', ], [ - 'route' => 'example.com' + 'route' => 'example.com', ] ); } - /** - * @group zf5656 - */ public function testFailedHostnameSegmentMatchDoesNotEmitErrors() { $this->expectException(RuntimeException::class); new Hostname(':subdomain.with_underscore.com'); } + + public function testRejectsNegativePathOffset() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $request = $this->prophesize(ServerRequestInterface::class); + $route = new Hostname('example.com'); + $route->partialMatch($request->reveal(), -1); + } } From 880cbc4803951d2458a66b3c40360f1783d7ad60 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Tue, 6 Mar 2018 19:50:53 +1000 Subject: [PATCH 21/47] Delete child route match --- src/Route/RouteMatch.php | 82 ----------------------------------- test/Route/RouteMatchTest.php | 68 ----------------------------- 2 files changed, 150 deletions(-) delete mode 100644 src/Route/RouteMatch.php delete mode 100644 test/Route/RouteMatchTest.php diff --git a/src/Route/RouteMatch.php b/src/Route/RouteMatch.php deleted file mode 100644 index cfabd1b..0000000 --- a/src/Route/RouteMatch.php +++ /dev/null @@ -1,82 +0,0 @@ -length = $length; - } - - /** - * setMatchedRouteName(): defined by BaseRouteMatch. - * - * @see BaseRouteMatch::setMatchedRouteName() - * @param string $name - * @return RouteMatch - */ - public function setMatchedRouteName($name) - { - if ($this->matchedRouteName === null) { - $this->matchedRouteName = $name; - } else { - $this->matchedRouteName = $name . '/' . $this->matchedRouteName; - } - - return $this; - } - - /** - * Merge parameters from another match. - * - * @param RouteMatch $match - * @return RouteMatch - */ - public function merge(RouteMatch $match) - { - $this->params = array_merge($this->params, $match->getParams()); - $this->length += $match->getLength(); - - $this->matchedRouteName = $match->getMatchedRouteName(); - - return $this; - } - - /** - * Get the matched path length. - * - * @return int - */ - public function getLength() - { - return $this->length; - } -} diff --git a/test/Route/RouteMatchTest.php b/test/Route/RouteMatchTest.php deleted file mode 100644 index dcc2ca7..0000000 --- a/test/Route/RouteMatchTest.php +++ /dev/null @@ -1,68 +0,0 @@ - 'bar']); - - $this->assertEquals(['foo' => 'bar'], $match->getParams()); - } - - public function testLengthIsStored() - { - $match = new RouteMatch([], 10); - - $this->assertEquals(10, $match->getLength()); - } - - public function testLengthIsMerged() - { - $match = new RouteMatch([], 10); - $match->merge(new RouteMatch([], 5)); - - $this->assertEquals(15, $match->getLength()); - } - - public function testMatchedRouteNameIsSet() - { - $match = new RouteMatch([]); - $match->setMatchedRouteName('foo'); - - $this->assertEquals('foo', $match->getMatchedRouteName()); - } - - public function testMatchedRouteNameIsPrependedWhenAlreadySet() - { - $match = new RouteMatch([]); - $match->setMatchedRouteName('foo'); - $match->setMatchedRouteName('bar'); - - $this->assertEquals('bar/foo', $match->getMatchedRouteName()); - } - - public function testMatchedRouteNameIsOverriddenOnMerge() - { - $match = new RouteMatch([]); - $match->setMatchedRouteName('foo'); - - $subMatch = new RouteMatch([]); - $subMatch->setMatchedRouteName('bar'); - - $match->merge($subMatch); - - $this->assertEquals('bar', $match->getMatchedRouteName()); - } -} From 354e5dd7a48885acdadcc3253760ab337750ffe3 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Tue, 6 Mar 2018 20:22:56 +1000 Subject: [PATCH 22/47] Refactor Method route for PSR-7 --- src/Route/Method.php | 85 +++++++++++-------------- test/Route/MethodTest.php | 128 ++++++++++++++++++++++++-------------- 2 files changed, 118 insertions(+), 95 deletions(-) diff --git a/src/Route/Method.php b/src/Route/Method.php index ba6efad..2b1f72d 100644 --- a/src/Route/Method.php +++ b/src/Route/Method.php @@ -9,16 +9,26 @@ namespace Zend\Router\Route; -use Traversable; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; use Zend\Router\Exception; +use Zend\Router\PartialRouteInterface; +use Zend\Router\PartialRouteResult; use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; + +use function array_map; +use function explode; +use function in_array; +use function is_array; +use function strtoupper; /** * Method route. */ -class Method implements RouteInterface +class Method implements PartialRouteInterface { + use PartialRouteTrait; + /** * Verb to match. * @@ -35,33 +45,22 @@ class Method implements RouteInterface /** * Create a new method route. - * - * @param string $verb - * @param array $defaults */ - public function __construct($verb, array $defaults = []) + public function __construct(string $verb, array $defaults = []) { - $this->verb = $verb; + $this->verb = $verb; $this->defaults = $defaults; } /** - * factory(): defined by RouteInterface interface. + * Create a new method route. * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $options - * @return Method * @throws Exception\InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []) : self { - if ($options instanceof Traversable) { + if (! is_array($options)) { $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'])) { @@ -75,52 +74,40 @@ public static function factory($options = []) return new static($options['verb'], $options['defaults']); } - /** - * match(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::match() - * @param Request $request - * @return RouteMatch|null - */ - public function match(Request $request) + public function partialMatch(Request $request, int $pathOffset = 0, array $options = []) : PartialRouteResult { - if (! method_exists($request, 'getMethod')) { - return; + if ($pathOffset < 0) { + throw new Exception\InvalidArgumentException('Path offset cannot be negative'); } - $requestVerb = strtoupper($request->getMethod()); - $matchVerbs = explode(',', strtoupper($this->verb)); - $matchVerbs = array_map('trim', $matchVerbs); + $matchVerbs = explode(',', strtoupper($this->verb)); + $matchVerbs = array_map('trim', $matchVerbs); if (in_array($requestVerb, $matchVerbs)) { - return new RouteMatch($this->defaults); + return PartialRouteResult::fromRouteMatch($this->defaults, $pathOffset, 0); } - return; + return PartialRouteResult::fromMethodFailure($matchVerbs, $pathOffset, 0); + } + + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface + { + return $uri; } /** - * assemble(): Defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed + * Method routes are not using parameters to assemble uri */ - public function assemble(array $params = [], array $options = []) + public function getLastAssembledParams() : array { - // The request method does not contribute to the path, thus nothing is returned. - return ''; + return []; } /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see RouteInterface::getAssembledParams - * @return array + * @deprecated */ - public function getAssembledParams() + public function getAssembledParams() : array { - return []; + return $this->getLastAssembledParams(); } } diff --git a/test/Route/MethodTest.php b/test/Route/MethodTest.php index 817077d..f42a9cd 100644 --- a/test/Route/MethodTest.php +++ b/test/Route/MethodTest.php @@ -10,76 +10,112 @@ namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; -use Zend\Http\Request; -use Zend\Router\Route\Method as HttpMethod; -use Zend\Router\Route\RouteMatch; -use Zend\Stdlib\Request as BaseRequest; +use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\ServerRequest; +use Zend\Diactoros\Uri; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\PartialRouteResult; +use Zend\Router\Route\Method; +use Zend\Router\RouteResult; use ZendTest\Router\FactoryTester; +use ZendTest\Router\Route\TestAsset\RouteTestDefinition; /** * @covers \Zend\Router\Route\Method */ class MethodTest extends TestCase { - public static function routeProvider() - { - return [ - 'simple-match' => [ - new HttpMethod('get'), - 'get' - ], - 'match-comma-separated-verbs' => [ - new HttpMethod('get,post'), - 'get' - ], - 'match-comma-separated-verbs-ws' => [ - new HttpMethod('get , post , put'), - 'post' - ], - 'match-ignores-case' => [ - new HttpMethod('Get'), - 'get' - ] - ]; - } + use PartialRouteTestTrait; + use RouteTestTrait; - /** - * @dataProvider routeProvider - * @param HttpMethod $route - * @param $verb - * @internal param string $path - * @internal param int $offset - * @internal param bool $shouldMatch - */ - public function testMatching(HttpMethod $route, $verb) + public function getRouteTestDefinitions() : iterable { - $request = new Request(); - $request->setUri('http://example.com'); - $request->setMethod($verb); + $request = new ServerRequest([], [], null, null, 'php://memory'); + + yield 'simple match' => (new RouteTestDefinition( + new Method('GET'), + $request->withMethod('GET') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 0) + ); - $match = $route->match($request); - $this->assertInstanceOf(RouteMatch::class, $match); + yield 'match comma separated verbs' => (new RouteTestDefinition( + new Method('get,post'), + $request->withMethod('POST') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 0) + ); + + yield 'match comma separated verbs with whitespace' => (new RouteTestDefinition( + new Method('get , post , put'), + $request->withMethod('POST') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 0) + ); + + yield 'match ignores case' => (new RouteTestDefinition( + new Method('Get'), + $request->withMethod('get') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 0) + ); + + yield 'no match gives list of allowed methods' => (new RouteTestDefinition( + new Method('POST,PUT,DELETE'), + $request->withMethod('GET') + )) + ->expectMatchResult( + RouteResult::fromMethodFailure(['POST', 'PUT', 'DELETE']) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromMethodFailure(['POST', 'PUT', 'DELETE'], 0, 0) + ); } - public function testNoMatchWithoutVerb() + public function testAssembleSimplyReturnsPassedUri() { - $route = new HttpMethod('get'); - $request = new BaseRequest(); + $uri = new Uri(); + $method = new Method('get'); - $this->assertNull($route->match($request)); + $this->assertSame($uri, $method->assemble($uri)); } public function testFactory() { $tester = new FactoryTester($this); $tester->testFactory( - HttpMethod::class, + Method::class, [ - 'verb' => 'Missing "verb" in options array' + 'verb' => 'Missing "verb" in options array', ], [ - 'verb' => 'get' + 'verb' => 'get', ] ); } + + public function testRejectsNegativePathOffset() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $request = $this->prophesize(ServerRequestInterface::class); + $route = new Method('GET'); + $route->partialMatch($request->reveal(), -1); + } } From c2df89b0f6f01755d7b3206b37a39e71f9019afb Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Tue, 6 Mar 2018 20:40:44 +1000 Subject: [PATCH 23/47] Refactor Scheme route for PSR-7 --- src/Route/Scheme.php | 87 ++++++++++++++------------------------- test/Route/SchemeTest.php | 70 ++++++++++++++++++++----------- 2 files changed, 77 insertions(+), 80 deletions(-) diff --git a/src/Route/Scheme.php b/src/Route/Scheme.php index 481cb92..9727268 100644 --- a/src/Route/Scheme.php +++ b/src/Route/Scheme.php @@ -9,16 +9,22 @@ namespace Zend\Router\Route; -use Traversable; -use Zend\Router\Exception; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\PartialRouteInterface; +use Zend\Router\PartialRouteResult; use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; + +use function is_array; /** * Scheme route. */ -class Scheme implements RouteInterface +class Scheme implements PartialRouteInterface { + use PartialRouteTrait; + /** * Scheme to match. * @@ -35,37 +41,24 @@ class Scheme implements RouteInterface /** * Create a new scheme route. - * - * @param string $scheme - * @param array $defaults */ - public function __construct($scheme, array $defaults = []) + public function __construct(string $scheme, array $defaults = []) { - $this->scheme = $scheme; + $this->scheme = $scheme; $this->defaults = $defaults; } /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $options - * @return Scheme - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []) : self { - if ($options instanceof Traversable) { + if (! is_array($options)) { $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'); + throw new InvalidArgumentException('Missing "scheme" in options array'); } if (! isset($options['defaults'])) { @@ -76,54 +69,38 @@ public static function factory($options = []) } /** - * match(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::match() - * @param Request $request - * @return RouteMatch|null + * @throws InvalidArgumentException */ - public function match(Request $request) + public function partialMatch(Request $request, int $pathOffset = 0, array $options = []) : PartialRouteResult { - if (! method_exists($request, 'getUri')) { - return; + if ($pathOffset < 0) { + throw new InvalidArgumentException('Path offset cannot be negative'); } - - $uri = $request->getUri(); + $uri = $request->getUri(); $scheme = $uri->getScheme(); if ($scheme !== $this->scheme) { - return; + return PartialRouteResult::fromRouteFailure(); } - return new RouteMatch($this->defaults); + return PartialRouteResult::fromRouteMatch($this->defaults, $pathOffset, 0); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { - if (isset($options['uri'])) { - $options['uri']->setScheme($this->scheme); - } + return $uri->withScheme($this->scheme); + } - // A scheme does not contribute to the path, thus nothing is returned. - return ''; + public function getLastAssembledParams() : array + { + return []; } /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see RouteInterface::getAssembledParams - * @return array + * @deprecated */ - public function getAssembledParams() + public function getAssembledParams() : array { - return []; + return $this->getLastAssembledParams(); } } diff --git a/test/Route/SchemeTest.php b/test/Route/SchemeTest.php index 1d46d5a..a9a5637 100644 --- a/test/Route/SchemeTest.php +++ b/test/Route/SchemeTest.php @@ -10,11 +10,11 @@ namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; -use Zend\Http\Request; -use Zend\Router\Route\RouteMatch; +use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\ServerRequest; +use Zend\Diactoros\Uri; +use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Route\Scheme; -use Zend\Stdlib\Request as BaseRequest; -use Zend\Uri\Http as HttpUri; use ZendTest\Router\FactoryTester; /** @@ -22,52 +22,63 @@ */ class SchemeTest extends TestCase { + /** + * @var ServerRequestInterface + */ + private $request; + + protected function setUp() + { + $this->request = new ServerRequest([], [], null, null, 'php://memory'); + } + public function testMatching() { - $request = new Request(); - $request->setUri('https://example.com/'); + $request = $this->request->withUri((new Uri())->withScheme('https')); $route = new Scheme('https'); - $match = $route->match($request); + $result = $route->match($request); - $this->assertInstanceOf(RouteMatch::class, $match); + $this->assertTrue($result->isSuccess()); } - public function testNoMatchingOnDifferentScheme() + public function testMatchReturnsResultWithDefaultParameters() { - $request = new Request(); - $request->setUri('http://example.com/'); + $request = $this->request->withUri((new Uri())->withScheme('https')); - $route = new Scheme('https'); - $match = $route->match($request); + $route = new Scheme('https', ['foo' => 'bar']); + $result = $route->match($request); - $this->assertNull($match); + $this->assertEquals(['foo' => 'bar'], $result->getMatchedParams()); } - public function testAssembling() + public function testNoMatchingOnDifferentScheme() { - $uri = new HttpUri(); + $request = $this->request->withUri((new Uri())->withScheme('http')); + $route = new Scheme('https'); - $path = $route->assemble([], ['uri' => $uri]); + $result = $route->match($request); - $this->assertEquals('', $path); - $this->assertEquals('https', $uri->getScheme()); + $this->assertTrue($result->isFailure()); } - public function testNoMatchWithoutUriMethod() + public function testAssembling() { - $route = new Scheme('https'); - $request = new BaseRequest(); + $uri = new Uri(); + $route = new Scheme('https'); + $resultUri = $route->assemble($uri); - $this->assertNull($route->match($request)); + $this->assertEquals('https', $resultUri->getScheme()); } public function testGetAssembledParams() { + $uri = new Uri(); $route = new Scheme('https'); - $route->assemble(['foo' => 'bar']); + $route->assemble($uri, ['foo' => 'bar']); - $this->assertEquals([], $route->getAssembledParams()); + $this->assertEquals([], $route->getLastAssembledParams()); + $this->assertEquals($route->getLastAssembledParams(), $route->getAssembledParams()); } public function testFactory() @@ -83,4 +94,13 @@ public function testFactory() ] ); } + + public function testRejectsNegativePathOffset() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $request = $this->prophesize(ServerRequestInterface::class); + $route = new Scheme('https'); + $route->partialMatch($request->reveal(), -1); + } } From 0ad59c3aa814df90e476a726c850e913e4cbab5c Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Wed, 7 Mar 2018 11:05:32 +1000 Subject: [PATCH 24/47] Refactor Segment route for PSR-7 --- src/Route/Segment.php | 225 +++++------ test/Route/SegmentTest.php | 785 +++++++++++++++++++++---------------- 2 files changed, 548 insertions(+), 462 deletions(-) diff --git a/src/Route/Segment.php b/src/Route/Segment.php index a90607f..29abd84 100644 --- a/src/Route/Segment.php +++ b/src/Route/Segment.php @@ -9,17 +9,34 @@ namespace Zend\Router\Route; -use Traversable; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; use Zend\I18n\Translator\TranslatorInterface as Translator; -use Zend\Router\Exception; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\Exception\RuntimeException; +use Zend\Router\PartialRouteInterface; +use Zend\Router\PartialRouteResult; use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; + +use function array_merge; +use function count; +use function is_array; +use function preg_match; +use function preg_quote; +use function rawurldecode; +use function rawurlencode; +use function sprintf; +use function str_replace; +use function strlen; +use function strtr; /** * Segment route. */ -class Segment implements RouteInterface +class Segment implements PartialRouteInterface { + use PartialRouteTrait; + /** * Cache for the encode output. * @@ -40,23 +57,23 @@ class Segment implements RouteInterface * @var array */ protected static $urlencodeCorrectionMap = [ - '%21' => "!", // sub-delims - '%24' => "$", // sub-delims - '%26' => "&", // sub-delims + '%21' => '!', // sub-delims + '%24' => '$', // sub-delims + '%26' => '&', // sub-delims '%27' => "'", // sub-delims - '%28' => "(", // sub-delims - '%29' => ")", // sub-delims - '%2A' => "*", // sub-delims - '%2B' => "+", // sub-delims - '%2C' => ",", // sub-delims -// '%2D' => "-", // unreserved - not touched by rawurlencode -// '%2E' => ".", // unreserved - not touched by rawurlencode - '%3A' => ":", // pchar - '%3B' => ";", // sub-delims - '%3D' => "=", // sub-delims - '%40' => "@", // pchar -// '%5F' => "_", // unreserved - not touched by rawurlencode -// '%7E' => "~", // unreserved - not touched by rawurlencode + '%28' => '(', // sub-delims + '%29' => ')', // sub-delims + '%2A' => '*', // sub-delims + '%2B' => '+', // sub-delims + '%2C' => ',', // sub-delims + // '%2D' => "-", // unreserved - not touched by rawurlencode + // '%2E' => ".", // unreserved - not touched by rawurlencode + '%3A' => ':', // pchar + '%3B' => ';', // sub-delims + '%3D' => '=', // sub-delims + '%40' => '@', // pchar + // '%5F' => "_", // unreserved - not touched by rawurlencode + // '%7E' => "~", // unreserved - not touched by rawurlencode ]; /** @@ -103,39 +120,27 @@ class Segment implements RouteInterface /** * Create a new regex route. - * - * @param string $route - * @param array $constraints - * @param array $defaults */ - 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); - $this->regex = $this->buildRegex($this->parts, $constraints); + $this->parts = $this->parseRouteDefinition($route); + $this->regex = $this->buildRegex($this->parts, $constraints); } /** * factory(): defined by RouteInterface interface. * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $options - * @return Segment - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []) : self { - if ($options instanceof Traversable) { + if (! is_array($options)) { $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'); + throw new InvalidArgumentException('Missing "route" in options array'); } if (! isset($options['constraints'])) { @@ -152,17 +157,15 @@ public static function factory($options = []) /** * Parse a route definition. * - * @param string $def - * @return array - * @throws Exception\RuntimeException + * @throws RuntimeException */ - protected function parseRouteDefinition($def) + protected function parseRouteDefinition(string $def) : array { $currentPos = 0; - $length = strlen($def); - $parts = []; + $length = strlen($def); + $parts = []; $levelParts = [&$parts]; - $level = 0; + $level = 0; while ($currentPos < $length) { preg_match('(\G(?P[^:{\[\]]*)(?P[:{\[\]]|$))', $def, $matches, 0, $currentPos); @@ -181,19 +184,19 @@ protected function parseRouteDefinition($def) 0, $currentPos )) { - throw new Exception\RuntimeException('Found empty parameter name'); + throw new RuntimeException('Found empty parameter name'); } $levelParts[$level][] = [ 'parameter', $matches['name'], - isset($matches['delimiters']) ? $matches['delimiters'] : null + isset($matches['delimiters']) ? $matches['delimiters'] : null, ]; $currentPos += strlen($matches[0]); } elseif ($matches['token'] === '{') { if (! preg_match('(\G(?P[^}]+)\})', $def, $matches, 0, $currentPos)) { - throw new Exception\RuntimeException('Translated literal missing closing bracket'); + throw new RuntimeException('Translated literal missing closing bracket'); } $currentPos += strlen($matches[0]); @@ -209,7 +212,7 @@ protected function parseRouteDefinition($def) $level--; if ($level < 0) { - throw new Exception\RuntimeException('Found closing bracket without matching opening bracket'); + throw new RuntimeException('Found closing bracket without matching opening bracket'); } } else { break; @@ -217,7 +220,7 @@ protected function parseRouteDefinition($def) } if ($level > 0) { - throw new Exception\RuntimeException('Found unbalanced brackets'); + throw new RuntimeException('Found unbalanced brackets'); } return $parts; @@ -225,13 +228,8 @@ protected function parseRouteDefinition($def) /** * Build the matching regex from parsed parts. - * - * @param array $parts - * @param array $constraints - * @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 = ''; @@ -272,29 +270,28 @@ protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1 /** * Build a path. * - * @param array $parts - * @param array $mergedParams - * @param bool $isOptional - * @param bool $hasChild - * @param array $options - * @return string - * @throws Exception\InvalidArgumentException - * @throws Exception\RuntimeException + * @throws InvalidArgumentException + * @throws 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) { - throw new Exception\RuntimeException('No translator provided'); + throw new RuntimeException('No translator provided'); } $translator = $options['translator']; - $textDomain = (isset($options['text_domain']) ? $options['text_domain'] : 'default'); - $locale = (isset($options['locale']) ? $options['locale'] : null); + $textDomain = isset($options['text_domain']) ? $options['text_domain'] : 'default'; + $locale = isset($options['locale']) ? $options['locale'] : null; } - $path = ''; - $skip = true; + $path = ''; + $skip = true; $skippable = false; foreach ($parts as $part) { @@ -308,7 +305,7 @@ protected function buildPath(array $parts, array $mergedParams, $isOptional, $ha if (! isset($mergedParams[$part[1]])) { if (! $isOptional || $hasChild) { - throw new Exception\InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1])); + throw new InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1])); } return ''; @@ -326,12 +323,12 @@ protected function buildPath(array $parts, array $mergedParams, $isOptional, $ha break; case 'optional': - $skippable = true; + $skippable = true; $optionalPart = $this->buildPath($part[1], $mergedParams, true, $hasChild, $options); if ($optionalPart !== '') { $path .= $optionalPart; - $skip = false; + $skip = false; } break; @@ -349,52 +346,42 @@ protected function buildPath(array $parts, array $mergedParams, $isOptional, $ha } /** - * match(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::match() - * @param Request $request - * @param string|null $pathOffset - * @param array $options - * @return RouteMatch|null - * @throws Exception\RuntimeException + * @throws InvalidArgumentException + * @throws RuntimeException */ - public function match(Request $request, $pathOffset = null, array $options = []) + public function partialMatch(Request $request, int $pathOffset = 0, array $options = []) : PartialRouteResult { - if (! method_exists($request, 'getUri')) { - return; + if ($pathOffset < 0) { + throw new InvalidArgumentException('Path offset cannot be negative'); } - - $uri = $request->getUri(); + $uri = $request->getUri(); $path = $uri->getPath(); $regex = $this->regex; if ($this->translationKeys) { if (! isset($options['translator']) || ! $options['translator'] instanceof Translator) { - throw new Exception\RuntimeException('No translator provided'); + throw new RuntimeException('No translator provided'); } $translator = $options['translator']; - $textDomain = (isset($options['text_domain']) ? $options['text_domain'] : 'default'); - $locale = (isset($options['locale']) ? $options['locale'] : null); + $textDomain = $options['text_domain'] ?? 'default'; + $locale = $options['locale'] ?? $options['parent_match_params'] ?? null; foreach ($this->translationKeys as $key) { $regex = str_replace('#' . $key . '#', $translator->translate($key, $textDomain, $locale), $regex); } } - if ($pathOffset !== null) { - $result = preg_match('(\G' . $regex . ')', $path, $matches, 0, $pathOffset); - } else { - $result = preg_match('(^' . $regex . '$)', $path, $matches); - } + // needs to be urlencoded to match urlencoded non-latin characters + $result = preg_match('(\G' . $regex . ')', $path, $matches, 0, $pathOffset); if (! $result) { - return; + return PartialRouteResult::fromRouteFailure(); } $matchedLength = strlen($matches[0]); - $params = []; + $params = []; foreach ($this->paramMap as $index => $name) { if (isset($matches[$index]) && $matches[$index] !== '') { @@ -402,48 +389,43 @@ public function match(Request $request, $pathOffset = null, array $options = []) } } - return new RouteMatch(array_merge($this->defaults, $params), $matchedLength); + return PartialRouteResult::fromRouteMatch(array_merge($this->defaults, $params), $pathOffset, $matchedLength); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { $this->assembledParams = []; - return $this->buildPath( + $path = $this->buildPath( $this->parts, array_merge($this->defaults, $params), false, - (isset($options['has_child']) ? $options['has_child'] : false), + isset($options['has_child']) ? $options['has_child'] : false, $options ); + + return $uri->withPath($uri->getPath() . $path); + } + + public function getLastAssembledParams() : array + { + return $this->assembledParams; } /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see RouteInterface::getAssembledParams - * @return array + * @deprecated */ - public function getAssembledParams() + public function getAssembledParams() : array { - return $this->assembledParams; + return $this->getLastAssembledParams(); } /** * Encode a path segment. * - * @param string $value - * @return string + * @todo replace with the version from diactoros */ - protected function encode(string $value) + protected function encode(string $value) : string { $key = (string) $value; if (! isset(static::$cacheEncode[$key])) { @@ -455,11 +437,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/test/Route/SegmentTest.php b/test/Route/SegmentTest.php index e02aebf..dc5e359 100644 --- a/test/Route/SegmentTest.php +++ b/test/Route/SegmentTest.php @@ -10,366 +10,462 @@ namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; -use Zend\Http\Request; +use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\ServerRequest; +use Zend\Diactoros\Uri; use Zend\I18n\Translator\Loader\FileLoaderInterface; use Zend\I18n\Translator\TextDomain; use Zend\I18n\Translator\Translator; use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Exception\RuntimeException; -use Zend\Router\Route\RouteMatch; +use Zend\Router\PartialRouteResult; +use Zend\Router\Route\PartialRouteTrait; use Zend\Router\Route\Segment; -use Zend\Stdlib\Request as BaseRequest; +use Zend\Router\RouteResult; use ZendTest\Router\FactoryTester; +use ZendTest\Router\Route\TestAsset\RouteTestDefinition; + +use function implode; /** * @covers \Zend\Router\Route\Segment */ class SegmentTest extends TestCase { - public function routeProvider() + use PartialRouteTrait; + use RouteTestTrait; + + public function getRouteTestDefinitions() : iterable { - return [ - 'simple-match' => [ - new Segment('/:foo'), - '/bar', - null, - ['foo' => 'bar'] - ], - 'no-match-without-leading-slash' => [ - new Segment(':foo'), - '/bar/', - null, - null - ], - 'no-match-with-trailing-slash' => [ - new Segment('/:foo'), - '/bar/', - null, - null - ], - 'offset-skips-beginning' => [ - new Segment(':foo'), - '/bar', - 1, - ['foo' => 'bar'] - ], - 'offset-enables-partial-matching' => [ - new Segment('/:foo'), - '/bar/baz', - 0, - ['foo' => 'bar'] - ], - 'match-overrides-default' => [ - new Segment('/:foo', [], ['foo' => 'baz']), - '/bar', - null, - ['foo' => 'bar'] - ], - 'constraints-prevent-match' => [ - new Segment('/:foo', ['foo' => '\d+']), - '/bar', - null, - null - ], - 'constraints-allow-match' => [ - new Segment('/:foo', ['foo' => '\d+']), - '/123', - null, - ['foo' => '123'] - ], - 'constraints-override-non-standard-delimiter' => [ - new Segment('/:foo{-}/bar', ['foo' => '[^/]+']), - '/foo-bar/bar', - null, - ['foo' => 'foo-bar'] - ], - 'constraints-with-parantheses-dont-break-parameter-map' => [ - new Segment('/:foo/:bar', ['foo' => '(bar)']), - '/bar/baz', - null, - ['foo' => 'bar', 'bar' => 'baz'] - ], - 'simple-match-with-optional-parameter' => [ - new Segment('/[:foo]', [], ['foo' => 'bar']), - '/', - null, - ['foo' => 'bar'] - ], - 'optional-parameter-is-ignored' => [ - new Segment('/:foo[/:bar]'), - '/bar', - null, - ['foo' => 'bar'] - ], - 'optional-parameter-is-provided-with-default' => [ - new Segment('/:foo[/:bar]', [], ['bar' => 'baz']), - '/bar', - null, - ['foo' => 'bar', 'bar' => 'baz'] - ], - 'optional-parameter-is-consumed' => [ - new Segment('/:foo[/:bar]'), - '/bar/baz', - null, - ['foo' => 'bar', 'bar' => 'baz'] - ], - 'optional-group-is-discared-with-missing-parameter' => [ - new Segment('/:foo[/:bar/:baz]', [], ['bar' => 'baz']), - '/bar', - null, - ['foo' => 'bar', 'bar' => 'baz'] - ], - 'optional-group-within-optional-group-is-ignored' => [ - new Segment('/:foo[/:bar[/:baz]]', [], ['bar' => 'baz', 'baz' => 'bat']), - '/bar', - null, - ['foo' => 'bar', 'bar' => 'baz', 'baz' => 'bat'] - ], - 'non-standard-delimiter-before-parameter' => [ - new Segment('/foo-:bar'), - '/foo-baz', - null, - ['bar' => 'baz'] - ], - 'non-standard-delimiter-between-parameters' => [ - new Segment('/:foo{-}-:bar'), - '/bar-baz', - null, - ['foo' => 'bar', 'bar' => 'baz'] - ], - 'non-standard-delimiter-before-optional-parameter' => [ - new Segment('/:foo{-/}[-:bar]/:baz'), - '/bar-baz/bat', - null, - ['foo' => 'bar', 'bar' => 'baz', 'baz' => 'bat'] - ], - 'non-standard-delimiter-before-ignored-optional-parameter' => [ - new Segment('/:foo{-/}[-:bar]/:baz'), - '/bar/bat', - null, - ['foo' => 'bar', 'baz' => 'bat'] - ], - 'parameter-with-dash-in-name' => [ - new Segment('/:foo-bar'), - '/baz', - null, - ['foo-bar' => 'baz'] - ], - 'url-encoded-parameters-are-decoded' => [ - new Segment('/:foo'), - '/foo%20bar', - null, - ['foo' => 'foo bar'] - ], - 'urlencode-flaws-corrected' => [ - new Segment('/:foo'), - "/!$&'()*,-.:;=@_~+", - null, - ['foo' => "!$&'()*,-.:;=@_~+"] - ], - 'empty-matches-are-replaced-with-defaults' => [ - new Segment('/foo[/:bar]/baz-:baz', [], ['bar' => 'bar']), - '/foo/baz-baz', - null, - ['bar' => 'bar', 'baz' => 'baz'] - ], - ]; + $params = ['foo' => 'bar']; + yield 'simple match' => (new RouteTestDefinition( + new Segment('/:foo'), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'no match without leading slash' => (new RouteTestDefinition( + new Segment(':foo'), + new Uri('/bar/') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + yield 'partial match with trailing slash' => (new RouteTestDefinition( + new Segment('/:foo'), + new Uri('/bar/') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch(['foo' => 'bar'], 0, 4) + ); + + $params = ['foo' => 'bar']; + yield 'offset skips beginning' => (new RouteTestDefinition( + new Segment(':foo'), + new Uri('/bar') + )) + ->usePathOffset(1) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 1, 3) + ); + + $params = ['foo' => 'bar', 'baz' => 'qux']; + yield 'match merges default parameters' => (new RouteTestDefinition( + new Segment('/:foo', [], ['baz' => 'qux']), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar']; + yield 'match overrides default parameters' => (new RouteTestDefinition( + new Segment('/:foo', [], ['foo' => 'baz']), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'constraints prevent match' => (new RouteTestDefinition( + new Segment('/:foo', ['foo' => '\d+']), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + $params = ['foo' => '123']; + yield 'constraints allow match' => (new RouteTestDefinition( + new Segment('/:foo', ['foo' => '\d+']), + new Uri('/123') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'foo-bar']; + yield 'constraints override non standard delimiter' => (new RouteTestDefinition( + new Segment('/:foo{-}/bar', ['foo' => '[^/]+']), + new Uri('/foo-bar/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar', 'bar' => 'baz']; + yield 'constraints with parentheses dont break parameter map' => (new RouteTestDefinition( + new Segment('/:foo/:bar', ['foo' => '(bar)']), + new Uri('/bar/baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'simple match with optional parameter' => (new RouteTestDefinition( + new Segment('/[:foo]', [], ['foo' => 'bar']), + new Uri('/') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch(['foo' => 'bar']) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch(['foo' => 'bar'], 0, 1) + ); + + yield 'optional parameter is ignored' => (new RouteTestDefinition( + new Segment('/:foo[/:baz]'), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch(['foo' => 'bar']) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch(['foo' => 'bar'], 0, 4) + ); + + $params = ['foo' => 'bar', 'bar' => 'baz']; + yield 'optional parameter is provided with default' => (new RouteTestDefinition( + new Segment('/:foo[/:bar]', [], ['bar' => 'baz']), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar', 'bar' => 'baz']; + yield 'optional parameter is consumed' => (new RouteTestDefinition( + new Segment('/:foo[/:bar]'), + new Uri('/bar/baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar', 'bar' => 'baz']; + yield 'optional group is discared with missing parameter' => (new RouteTestDefinition( + new Segment('/:foo[/:bar/:baz]', [], ['bar' => 'baz']), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar', 'bar' => 'baz', 'baz' => 'bat']; + yield 'optional group within optional group is ignored' => (new RouteTestDefinition( + new Segment('/:foo[/:bar[/:baz]]', [], ['bar' => 'baz', 'baz' => 'bat']), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['bar' => 'baz']; + yield 'non standard delimiter before parameter' => (new RouteTestDefinition( + new Segment('/foo-:bar'), + new Uri('/foo-baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar', 'bar' => 'baz']; + yield 'non standard delimiter between parameters' => (new RouteTestDefinition( + new Segment('/:foo{-}-:bar'), + new Uri('/bar-baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar', 'bar' => 'baz', 'baz' => 'bat']; + yield 'non standard delimiter before optional parameter' => (new RouteTestDefinition( + new Segment('/:foo{-/}[-:bar]/:baz'), + new Uri('/bar-baz/bat') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'bar', 'baz' => 'bat']; + yield 'non standard delimiter before ignored optional parameter' => (new RouteTestDefinition( + new Segment('/:foo{-/}[-:bar]/:baz'), + new Uri('/bar/bat') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo-bar' => 'baz']; + yield 'parameter with dash in name' => (new RouteTestDefinition( + new Segment('/:foo-bar'), + new Uri('/baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => 'foo bar']; + yield 'url encoded parameters are decoded' => (new RouteTestDefinition( + new Segment('/:foo'), + new Uri('/foo%20bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['foo' => "!$&'()*,-.:;=@_~+"]; + yield 'urlencode flaws corrected' => (new RouteTestDefinition( + new Segment('/:foo'), + new Uri("/!$&'()*,-.:;=@_~+") + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['bar' => 'bar', 'baz' => 'baz']; + yield 'empty matches are replaced with defaults' => (new RouteTestDefinition( + new Segment('/foo[/:bar]/baz-:baz', [], ['bar' => 'bar']), + new Uri('/foo/baz-baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield from $this->getL10nRouteTestDefinitions(); } - public function l10nRouteProvider() + public function getL10nRouteTestDefinitions() : iterable { - $this->markTestIncomplete( - 'Translation tests need to be updated once zend-i18n is updated for zend-servicemanager v3' - ); - // @codingStandardsIgnoreStart $translator = new Translator(); $translator->setLocale('en-US'); - $enLoader = $this->getMock(FileLoaderInterface::class); - $deLoader = $this->getMock(FileLoaderInterface::class); - $domainLoader = $this->getMock(FileLoaderInterface::class); + $enLoader = $this->createMock(FileLoaderInterface::class); + $deLoader = $this->createMock(FileLoaderInterface::class); + $domainLoader = $this->createMock(FileLoaderInterface::class); $enLoader->expects($this->any())->method('load')->willReturn(new TextDomain(['fw' => 'framework'])); $deLoader->expects($this->any())->method('load')->willReturn(new TextDomain(['fw' => 'baukasten'])); $domainLoader->expects($this->any())->method('load')->willReturn(new TextDomain(['fw' => 'fw-alternative'])); - $translator->getPluginManager()->setService('test-en', $enLoader); - $translator->getPluginManager()->setService('test-de', $deLoader); + $translator->getPluginManager()->setService('test-en', $enLoader); + $translator->getPluginManager()->setService('test-de', $deLoader); $translator->getPluginManager()->setService('test-domain', $domainLoader); $translator->addTranslationFile('test-en', null, 'default', 'en-US'); $translator->addTranslationFile('test-de', null, 'default', 'de-DE'); $translator->addTranslationFile('test-domain', null, 'alternative', 'en-US'); // @codingStandardsIgnoreEnd - return [ - 'translate-with-default-locale' => [ - new Segment('/{fw}', [], []), - '/framework', - null, - [], - ['translator' => $translator] - ], - 'translate-with-specific-locale' => [ - new Segment('/{fw}', [], []), - '/baukasten', - null, - [], - ['translator' => $translator, 'locale' => 'de-DE'] - ], - 'translate-uses-message-id-as-fallback' => [ - new Segment('/{fw}', [], []), - '/fw', - null, - [], - ['translator' => $translator, 'locale' => 'fr-FR'] - ], - 'translate-with-specific-text-domain' => [ - new Segment('/{fw}', [], []), - '/fw-alternative', - null, - [], - ['translator' => $translator, 'text_domain' => 'alternative'] - ], - ]; + yield 'translate with default locale' => (new RouteTestDefinition( + new Segment('/{fw}', [], []), + new Uri('/framework') + )) + ->useMatchOptions(['translator' => $translator]) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 10) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useOptionsForAssemble(['translator' => $translator]); + + yield 'translate with default locale' => (new RouteTestDefinition( + new Segment('/{fw}', [], []), + new Uri('/baukasten') + )) + ->useMatchOptions(['translator' => $translator, 'locale' => 'de-DE']) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 10) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useOptionsForAssemble(['translator' => $translator, 'locale' => 'de-DE']); + + yield 'translate uses message id as fallback' => (new RouteTestDefinition( + new Segment('/{fw}', [], []), + new Uri('/fw') + )) + ->useMatchOptions(['translator' => $translator, 'locale' => 'fr-FR']) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 10) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useOptionsForAssemble(['translator' => $translator, 'locale' => 'fr-FR']); + + yield 'translate with specific text domain' => (new RouteTestDefinition( + new Segment('/{fw}', [], []), + new Uri('/fw-alternative') + )) + ->useMatchOptions(['translator' => $translator, 'text_domain' => 'alternative']) + ->expectMatchResult( + RouteResult::fromRouteMatch([]) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch([], 0, 10) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useOptionsForAssemble(['translator' => $translator, 'text_domain' => 'alternative']); } - public static function parseExceptionsProvider() + public static function parseExceptionsProvider() : array { return [ 'unbalanced-brackets' => [ '[', RuntimeException::class, - 'Found unbalanced brackets' + 'Found unbalanced brackets', ], 'closing-bracket-without-opening-bracket' => [ ']', RuntimeException::class, - 'Found closing bracket without matching opening bracket' + 'Found closing bracket without matching opening bracket', ], 'empty-parameter-name' => [ ':', RuntimeException::class, - 'Found empty parameter name' + 'Found empty parameter name', ], 'translated-literal-without-closing-backet' => [ '{test', RuntimeException::class, - 'Translated literal missing closing bracket' + 'Translated literal missing closing bracket', ], ]; } - /** - * @dataProvider routeProvider - * @param Segment $route - * @param string $path - * @param int $offset - * @param array $params - * @param array $options - */ - public function testMatching(Segment $route, $path, $offset, array $params = null, array $options = []) - { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset, $options); - - 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)); - } - } - } - - /** - * @dataProvider routeProvider - * @param Segment $route - * @param string $path - * @param int $offset - * @param array $params - * @param array $options - */ - public function testAssembling(Segment $route, $path, $offset, array $params = null, array $options = []) - { - 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, $result, $offset)); - } else { - $this->assertEquals($path, $result); - } - } - - /** - * @dataProvider l10nRouteProvider - * @param Segment $route - * @param string $path - * @param int $offset - * @param array $params - * @param array $options - */ - public function testMatchingWithL10n(Segment $route, $path, $offset, array $params = null, array $options = []) - { - $request = new Request(); - $request->setUri('http://example.com' . $path); - $match = $route->match($request, $offset, $options); - - 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)); - } - } - } - - /** - * @dataProvider l10nRouteProvider - * @param Segment $route - * @param string $path - * @param int $offset - * @param array $params - * @param array $options - */ - public function testAssemblingWithL10n(Segment $route, $path, $offset, array $params = null, array $options = []) - { - 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, $result, $offset)); - } else { - $this->assertEquals($path, $result); - } - } - /** * @dataProvider parseExceptionsProvider - * @param string $route - * @param string $exceptionName - * @param string $exceptionMessage */ - public function testParseExceptions($route, $exceptionName, $exceptionMessage) + public function testParseExceptions(string $route, string $exceptionName, string $exceptionMessage) { $this->expectException($exceptionName); $this->expectExceptionMessage($exceptionMessage); @@ -378,20 +474,22 @@ public function testParseExceptions($route, $exceptionName, $exceptionMessage) public function testAssemblingWithMissingParameterInRoot() { + $uri = new Uri(); $route = new Segment('/:foo'); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing parameter "foo"'); - $route->assemble(); + $route->assemble($uri); } public function testTranslatedAssemblingThrowsExceptionWithoutTranslator() { + $uri = new Uri(); $route = new Segment('/{foo}'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('No translator provided'); - $route->assemble(); + $route->assemble($uri); } public function testTranslatedMatchingThrowsExceptionWithoutTranslator() @@ -400,36 +498,37 @@ public function testTranslatedMatchingThrowsExceptionWithoutTranslator() $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 ServerRequest()); } public function testAssemblingWithExistingChild() { + $uri = new Uri(); $route = new Segment('/[:foo]', [], ['foo' => 'bar']); - $path = $route->assemble([], ['has_child' => true]); + $path = $route->assemble($uri, [], ['has_child' => true]); $this->assertEquals('/bar', $path); } + public function testGetAssembledParams() + { + $uri = new Uri(); + $route = new Segment('/:foo'); + $route->assemble($uri, ['foo' => 'bar', 'baz' => 'bat']); + $this->assertEquals(['foo'], $route->getLastAssembledParams()); + } + public function testFactory() { $tester = new FactoryTester($this); $tester->testFactory( Segment::class, [ - 'route' => 'Missing "route" in options array' + 'route' => 'Missing "route" in options array', ], [ - 'route' => '/:foo[/:bar{-}]', - 'constraints' => ['foo' => 'bar'] + 'route' => '/:foo[/:bar{-}]', + 'constraints' => ['foo' => 'bar'], ] ); } @@ -439,46 +538,54 @@ 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/' . $raw); - $route = new Segment('/:foo'); - $match = $route->match($request); + $request = new ServerRequest([], [], new Uri('http://example.com/' . $raw)); + $route = new Segment('/:foo'); + $result = $route->match($request); - $this->assertSame($raw, $match->getParam('foo')); + $this->assertTrue($result->isSuccess()); + $this->assertSame($raw, $result->getMatchedParams()['foo']); } public function testEncodedDecode() { // @codingStandardsIgnoreStart // 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'; + $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`-=[]\\;\',./~!@#$%^&*()_+{}|:"<>?'; // @codingStandardsIgnoreEnd - $request = new Request(); - $request->setUri('http://example.com/' . $in); - $route = new Segment('/:foo'); - $match = $route->match($request); + $request = new ServerRequest([], [], new Uri('http://example.com/' . $in)); + $route = new Segment('/:foo'); + $result = $route->match($request); - $this->assertSame($out, $match->getParam('foo')); + $this->assertTrue($result->isSuccess()); + $this->assertSame($out, $result->getMatchedParams()['foo']); } public function testEncodeCache() { + $uri = new Uri(); $params1 = ['p1' => 6.123, 'p2' => 7]; - $uri1 = 'example.com/'.implode('/', $params1); + $uri1 = 'example.com/' . implode('/', $params1); $params2 = ['p1' => 6, 'p2' => 'test']; - $uri2 = 'example.com/'.implode('/', $params2); + $uri2 = 'example.com/' . implode('/', $params2); $route = new Segment('example.com/:p1/:p2'); - $request = new Request(); - - $request->setUri($uri1); + $request = new ServerRequest([], [], new Uri($uri1)); $route->match($request); - $this->assertSame($uri1, $route->assemble($params1)); + $this->assertSame($uri1, $route->assemble($uri, $params1)->getPath()); - $request->setUri($uri2); + $request = $request->withUri(new Uri($uri2)); $route->match($request); - $this->assertSame($uri2, $route->assemble($params2)); + $this->assertSame($uri2, $route->assemble($uri, $params2)->getPath()); + } + + public function testRejectsNegativePathOffset() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $request = $this->prophesize(ServerRequestInterface::class); + $route = new Segment('/foo'); + $route->partialMatch($request->reveal(), -1); } } From 57214388b7d16f18778abec74805fcdce3df02af Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Wed, 7 Mar 2018 11:49:25 +1000 Subject: [PATCH 25/47] Refactor Regex route for PSR-7 --- src/Route/Regex.php | 108 ++++++++---------- test/Route/RegexTest.php | 232 +++++++++++++++++++-------------------- 2 files changed, 159 insertions(+), 181 deletions(-) diff --git a/src/Route/Regex.php b/src/Route/Regex.php index 1a1559e..8007c12 100644 --- a/src/Route/Regex.php +++ b/src/Route/Regex.php @@ -9,16 +9,31 @@ namespace Zend\Router\Route; -use Traversable; -use Zend\Router\Exception; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\PartialRouteInterface; +use Zend\Router\PartialRouteResult; use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; + +use function array_merge; +use function is_array; +use function is_int; +use function is_numeric; +use function preg_match; +use function rawurldecode; +use function rawurlencode; +use function str_replace; +use function strlen; +use function strpos; /** * Regex route. */ -class Regex implements RouteInterface +class Regex implements PartialRouteInterface { + use PartialRouteTrait; + /** * Regex to match. * @@ -51,43 +66,29 @@ class Regex implements RouteInterface /** * Create a new regex route. - * - * @param string $regex - * @param string $spec - * @param array $defaults */ - public function __construct($regex, $spec, array $defaults = []) + public function __construct(string $regex, string $spec, array $defaults = []) { - $this->regex = $regex; - $this->spec = $spec; + $this->regex = $regex; + $this->spec = $spec; $this->defaults = $defaults; } /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param array|Traversable $options - * @return Regex - * @throws \Zend\Router\Exception\InvalidArgumentException + * @throws InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []) : self { - if ($options instanceof Traversable) { + if (! is_array($options)) { $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'); + throw new InvalidArgumentException('Missing "regex" in options array'); } if (! isset($options['spec'])) { - throw new Exception\InvalidArgumentException('Missing "spec" in options array'); + throw new InvalidArgumentException('Missing "spec" in options array'); } if (! isset($options['defaults'])) { @@ -98,29 +99,20 @@ public static function factory($options = []) } /** - * match(): defined by RouteInterface interface. - * - * @param Request $request - * @param int $pathOffset - * @return RouteMatch|null + * @throws InvalidArgumentException */ - public function match(Request $request, $pathOffset = null) + public function partialMatch(Request $request, int $pathOffset = 0, array $options = []) : PartialRouteResult { - if (! method_exists($request, 'getUri')) { - return; + if ($pathOffset < 0) { + throw new InvalidArgumentException('Path offset cannot be negative'); } - - $uri = $request->getUri(); + $uri = $request->getUri(); $path = $uri->getPath(); - if ($pathOffset !== null) { - $result = preg_match('(\G' . $this->regex . ')', $path, $matches, 0, $pathOffset); - } else { - $result = preg_match('(^' . $this->regex . '$)', $path, $matches); - } + $result = preg_match('(\G' . $this->regex . ')', $path, $matches, 0, $pathOffset); if (! $result) { - return; + return PartialRouteResult::fromRouteFailure(); } $matchedLength = strlen($matches[0]); @@ -133,21 +125,13 @@ public function match(Request $request, $pathOffset = null) } } - return new RouteMatch(array_merge($this->defaults, $matches), $matchedLength); + return PartialRouteResult::fromRouteMatch(array_merge($this->defaults, $matches), $pathOffset, $matchedLength); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { - $url = $this->spec; - $mergedParams = array_merge($this->defaults, $params); + $url = $this->spec; + $mergedParams = array_merge($this->defaults, $params); $this->assembledParams = []; foreach ($mergedParams as $key => $value) { @@ -160,17 +144,19 @@ public function assemble(array $params = [], array $options = []) } } - return $url; + return $uri->withPath($uri->getPath() . $url); + } + + public function getLastAssembledParams() : array + { + return $this->assembledParams; } /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see RouteInterface::getAssembledParams - * @return array + * @deprecated */ - public function getAssembledParams() + public function getAssembledParams() : array { - return $this->assembledParams; + return $this->getLastAssembledParams(); } } diff --git a/test/Route/RegexTest.php b/test/Route/RegexTest.php index 657169d..35c90ad 100644 --- a/test/Route/RegexTest.php +++ b/test/Route/RegexTest.php @@ -10,130 +10,114 @@ namespace ZendTest\Router\Route; use PHPUnit\Framework\TestCase; -use Zend\Http\Request; +use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\ServerRequest; +use Zend\Diactoros\Uri; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\PartialRouteResult; use Zend\Router\Route\Regex; -use Zend\Router\Route\RouteMatch; -use Zend\Stdlib\Request as BaseRequest; +use Zend\Router\RouteResult; use ZendTest\Router\FactoryTester; +use ZendTest\Router\Route\TestAsset\RouteTestDefinition; /** * @covers \Zend\Router\Route\Regex */ class RegexTest extends TestCase { - public static function routeProvider() - { - return [ - 'simple-match' => [ - new Regex('/(?[^/]+)', '/%foo%'), - '/bar', - null, - ['foo' => 'bar'] - ], - 'no-match-without-leading-slash' => [ - new Regex('(?[^/]+)', '%foo%'), - '/bar', - null, - null - ], - 'no-match-with-trailing-slash' => [ - new Regex('/(?[^/]+)', '/%foo%'), - '/bar/', - null, - null - ], - 'offset-skips-beginning' => [ - new Regex('(?[^/]+)', '%foo%'), - '/bar', - 1, - ['foo' => 'bar'] - ], - 'offset-enables-partial-matching' => [ - new Regex('/(?[^/]+)', '/%foo%'), - '/bar/baz', - 0, - ['foo' => 'bar'] - ], - 'url-encoded-parameters-are-decoded' => [ - new Regex('/(?[^/]+)', '/%foo%'), - '/foo%20bar', - null, - ['foo' => 'foo bar'] - ], - 'empty-matches-are-replaced-with-defaults' => [ - new Regex('/foo(?:/(?[^/]+))?/baz-(?[^/]+)', '/foo/baz-%baz%', ['bar' => 'bar']), - '/foo/baz-baz', - null, - ['bar' => 'bar', 'baz' => 'baz'] - ], - ]; - } + use PartialRouteTestTrait; + use RouteTestTrait; - /** - * @dataProvider routeProvider - * @param Regex $route - * @param string $path - * @param int $offset - * @param array $params - */ - public function testMatching(Regex $route, $path, $offset, array $params = null) + public function getRouteTestDefinitions() : iterable { - $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)); - } - } - } - - /** - * @dataProvider routeProvider - * @param Regex $route - * @param string $path - * @param int $offset - * @param array $params - */ - public function testAssembling(Regex $route, $path, $offset, array $params = null) - { - if ($params === null) { - // Data which will not match are not tested for assembling. - return; - } - - $result = $route->assemble($params); - - if ($offset !== null) { - $this->assertEquals($offset, strpos($path, $result, $offset)); - } else { - $this->assertEquals($path, $result); - } - } - - public function testNoMatchWithoutUriMethod() - { - $route = new Regex('/foo', '/foo'); - $request = new BaseRequest(); - - $this->assertNull($route->match($request)); + $params = ['foo' => 'bar']; + yield 'simple match' => (new RouteTestDefinition( + new Regex('/(?[^/]+)', '/%foo%'), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + yield 'no match without leading slash' => (new RouteTestDefinition( + new Regex('(?[^/]+)', '%foo%'), + new Uri('/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteFailure() + ); + + yield 'only partial match with trailing slash' => (new RouteTestDefinition( + new Regex('/(?[^/]+)', '/%foo%'), + new Uri('/bar/') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch(['foo' => 'bar'], 0, 4) + ); + + $params = ['foo' => 'bar']; + yield 'offset skips beginning' => (new RouteTestDefinition( + new Regex('(?[^/]+)', '%foo%'), + new Uri('/bar') + )) + ->usePathOffset(1) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 1, 3) + ) + ->shouldAssembleAndExpectResult(new Uri('bar')) + ->useParamsForAssemble($params); + + $params = ['foo' => 'foo bar']; + yield 'url encoded parameters are decoded' => (new RouteTestDefinition( + new Regex('/(?[^/]+)', '/%foo%'), + new Uri('/foo%20bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 10) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['bar' => 'bar', 'baz' => 'baz']; + yield 'empty matches are replaced with defaults' => (new RouteTestDefinition( + new Regex('/foo(?:/(?[^/]+))?/baz-(?[^/]+)', '/foo/baz-%baz%', ['bar' => 'bar']), + new Uri('/foo/baz-baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 12) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); } public function testGetAssembledParams() { + $uri = new Uri(); $route = new Regex('/(?.+)', '/%foo%'); - $route->assemble(['foo' => 'bar', 'baz' => 'bat']); + $route->assemble($uri, ['foo' => 'bar', 'baz' => 'bat']); - $this->assertEquals(['foo'], $route->getAssembledParams()); + $this->assertEquals(['foo'], $route->getLastAssembledParams()); + $this->assertEquals($route->getLastAssembledParams(), $route->getAssembledParams()); } public function testFactory() @@ -143,11 +127,11 @@ public function testFactory() Regex::class, [ 'regex' => 'Missing "regex" in options array', - 'spec' => 'Missing "spec" in options array' + 'spec' => 'Missing "spec" in options array', ], [ 'regex' => '/foo', - 'spec' => '/foo' + 'spec' => '/foo', ] ); } @@ -157,12 +141,12 @@ 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/' . $raw); - $route = new Regex('/(?[^/]+)', '/%foo%'); - $match = $route->match($request); + $request = new ServerRequest([], [], new Uri('http://example.com/' . $raw)); + $route = new Regex('/(?[^/]+)', '/%foo%'); + $result = $route->match($request); - $this->assertSame($raw, $match->getParam('foo')); + $this->assertTrue($result->isSuccess()); + $this->assertSame($raw, $result->getMatchedParams()['foo']); } public function testEncodedDecode() @@ -173,11 +157,19 @@ 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 ServerRequest([], [], new Uri('http://example.com/' . $in)); + $route = new Regex('/(?[^/]+)', '/%foo%'); + $result = $route->match($request); + + $this->assertSame($out, $result->getMatchedParams()['foo']); + } - $this->assertSame($out, $match->getParam('foo')); + public function testRejectsNegativePathOffset() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $request = $this->prophesize(ServerRequestInterface::class); + $route = new Regex('/foo', '/%foo%'); + $route->partialMatch($request->reveal(), -1); } } From 1e31a6f60847d8023377abea1271b54530cf7e14 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Wed, 7 Mar 2018 22:51:32 +1000 Subject: [PATCH 26/47] Update PartialRouteResult and add allowed methods for successful match --- src/PartialRouteResult.php | 81 +++++++++++++++++++++++++-------- test/PartialRouteResultTest.php | 50 ++++++++++++++------ 2 files changed, 99 insertions(+), 32 deletions(-) diff --git a/src/PartialRouteResult.php b/src/PartialRouteResult.php index 9fde5a0..273227f 100644 --- a/src/PartialRouteResult.php +++ b/src/PartialRouteResult.php @@ -10,12 +10,14 @@ namespace Zend\Router; use Psr\Http\Message\UriInterface; -use Zend\Router\Exception\DomainException; +use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Exception\RuntimeException; use function array_change_key_case; use function array_flip; use function array_keys; +use function sprintf; +use function strlen; use const CASE_UPPER; @@ -41,7 +43,7 @@ final class PartialRouteResult /** * Matched route name. * - * @var string|null + * @var null|string */ private $matchedRouteName; @@ -55,34 +57,46 @@ final class PartialRouteResult */ private $pathOffset = 0; + /** + * @var null|string[] + */ + private $matchedAllowedMethods; + /** * Create successful routing result + * + * @throws InvalidArgumentException */ public static function fromRouteMatch( array $matchedParams, int $pathOffset, int $matchedPathLength, - string $routeName = null - ) : PartialRouteResult { + string $routeName = null, + array $allowedMethods = null + ) : self { if ($pathOffset < 0) { - throw new DomainException('Path offset cannot be negative'); + throw new InvalidArgumentException('Path offset cannot be negative'); } if ($matchedPathLength < 0) { - throw new DomainException('Matched path length cannot be negative'); + throw new InvalidArgumentException('Matched path length cannot be negative'); } + $result = new self(); $result->success = true; $result->matchedParams = $matchedParams; $result->matchedRouteName = $routeName; $result->pathOffset = $pathOffset; $result->matchedPathLength = $matchedPathLength; + if (! empty($allowedMethods)) { + $result->setMatchedAllowedMethods($allowedMethods); + } return $result; } /** * Create failed routing result */ - public static function fromRouteFailure() : PartialRouteResult + public static function fromRouteFailure() : self { $result = new self(); $result->success = false; @@ -92,20 +106,22 @@ public static function fromRouteFailure() : PartialRouteResult /** * Create routing failure result where http method is not allowed for the * otherwise routable request + * + * @throws InvalidArgumentException */ public static function fromMethodFailure( array $allowedMethods, int $pathOffset, int $matchedPathLength - ) : PartialRouteResult { + ) : self { if (empty($allowedMethods)) { - throw new DomainException('Method failure requires list of allowed methods'); + throw new InvalidArgumentException('Method failure requires list of allowed methods'); } if ($pathOffset < 0) { - throw new DomainException('Path offset cannot be negative'); + throw new InvalidArgumentException('Path offset cannot be negative'); } if ($matchedPathLength < 0) { - throw new DomainException('Matched path length cannot be negative'); + throw new InvalidArgumentException('Matched path length cannot be negative'); } $result = new self(); @@ -162,14 +178,16 @@ public function isFullPathMatch(UriInterface $uri) : bool * with successful result. * * @param string $flag Signifies mode of setting route name: - * - {@see RouteResult::NAME_REPLACE} replaces existing route name - * - {@see RouteResult::NAME_PREPEND} prepends as a parent route part name. - * - {@see RouteResult::NAME_APPEND} appends as a child route part name. + * - {@see RouteResult::NAME_REPLACE} replaces existing route name + * - {@see RouteResult::NAME_PREPEND} prepends as a parent route part name. + * - {@see RouteResult::NAME_APPEND} appends as a child route part name. + * @throws InvalidArgumentException + * @throws RuntimeException */ - public function withMatchedRouteName(string $routeName, $flag = RouteResult::NAME_REPLACE) : PartialRouteResult + public function withMatchedRouteName(string $routeName, $flag = RouteResult::NAME_REPLACE) : self { if (empty($routeName)) { - throw new DomainException('Route name cannot be empty'); + throw new InvalidArgumentException('Route name cannot be empty'); } if (! $this->isSuccess()) { throw new RuntimeException('Only successful routing can have matched route name'); @@ -187,7 +205,7 @@ public function withMatchedRouteName(string $routeName, $flag = RouteResult::NAM } elseif ($flag === RouteResult::NAME_APPEND) { $routeName = sprintf('%s/%s', $this->matchedRouteName, $routeName); } else { - throw new DomainException('Unknown flag for setting matched route name'); + throw new InvalidArgumentException('Unknown flag for setting matched route name'); } $result->matchedRouteName = $routeName; @@ -197,8 +215,10 @@ public function withMatchedRouteName(string $routeName, $flag = RouteResult::NAM /** * Produce a new partial route result with provided matched parameters. Can only be * used with successful result. + * + * @throws RuntimeException */ - public function withMatchedParams(array $params) : PartialRouteResult + public function withMatchedParams(array $params) : self { if (! $this->isSuccess()) { throw new RuntimeException('Only successful routing can have matched params'); @@ -250,6 +270,31 @@ public function getMatchedPathLength() : int return $this->matchedPathLength; } + public function getMatchedAllowedMethods() : ?array + { + return $this->matchedAllowedMethods; + } + + public function withMatchedAllowedMethods(array $methods) : self + { + $result = clone $this; + $result->setMatchedAllowedMethods($methods); + + return $result; + } + + /** + * Helper function to deduplicate and normalize HTTP method names + */ + private function setMatchedAllowedMethods(array $methods) : void + { + $methods = array_keys(array_change_key_case( + array_flip($methods), + CASE_UPPER + )); + $this->matchedAllowedMethods = $methods; + } + /** * Helper function to deduplicate and normalize HTTP method names */ diff --git a/test/PartialRouteResultTest.php b/test/PartialRouteResultTest.php index cf51136..bf38e73 100644 --- a/test/PartialRouteResultTest.php +++ b/test/PartialRouteResultTest.php @@ -4,13 +4,14 @@ * @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (http://www.zend.com) * @license https://github.com/zendframework/zend-router/blob/master/LICENSE.md New BSD License */ + declare(strict_types=1); namespace ZendTest\Router; use PHPUnit\Framework\TestCase; use Zend\Diactoros\Uri; -use Zend\Router\Exception\DomainException; +use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\Exception\RuntimeException; use Zend\Router\PartialRouteResult; use Zend\Router\RouteResult; @@ -49,14 +50,14 @@ public function testFromMethodFailureDeduplicatesAndNormalizesHttpMethods() public function testFromMethodFailureRejectsNegativeOffset() { - $this->expectException(DomainException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Path offset cannot be negative'); $result = PartialRouteResult::fromMethodFailure(['GET'], -1, 0); } public function testFromMethodFailureRejectsNegativeMatchedLength() { - $this->expectException(DomainException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Matched path length cannot be negative'); PartialRouteResult::fromMethodFailure(['GET'], 0, -1); } @@ -68,7 +69,7 @@ public function testFromMethodFailureRejectsNegativeMatchedLength() */ public function testFromMethodFailureThrowsOnEmptyAllowedMethodsList() { - $this->expectException(DomainException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Method failure requires list of allowed methods'); PartialRouteResult::fromMethodFailure([], 10, 20); } @@ -109,14 +110,14 @@ public function testFromRouteMatchSetsMatchedParameters() public function testFromRouteMatchRejectsNegativeOffset() { - $this->expectException(DomainException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Path offset cannot be negative'); PartialRouteResult::fromRouteMatch([], -1, 0); } public function testFromRouteMatchRejectsNegativeMatchedLength() { - $this->expectException(DomainException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Matched path length cannot be negative'); PartialRouteResult::fromRouteMatch([], 0, -1); } @@ -180,7 +181,7 @@ public function testWithRouteNameThrowsForUnsuccessfulResult() public function testWithRouteNameRejectsEmptyName() { - $this->expectException(DomainException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Route name cannot be empty'); $result = PartialRouteResult::fromRouteMatch([], 0, 0, 'foo'); $result->withMatchedRouteName(''); @@ -188,7 +189,7 @@ public function testWithRouteNameRejectsEmptyName() public function testWithRouteNameThrowsOnUnknownFlag() { - $this->expectException(DomainException::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Unknown flag'); $result = PartialRouteResult::fromRouteMatch([], 0, 0, 'foo'); $result->withMatchedRouteName('bar', 'unknown'); @@ -213,42 +214,63 @@ public function testWithMatchedParamsThrowsForUnsuccessfulResult() $result->withMatchedParams(['foo' => 'bar']); } - public function provideFullPathMatchData() + public function provideFullPathMatchData() : array { return [ 'full match' => [ new Uri('/foo'), 0, 4, - true + true, ], 'partial match' => [ new Uri('/foo'), 0, 3, - false + false, ], 'offset full match' => [ new Uri('/foo'), 1, 3, - true + true, ], 'offset partial match' => [ new Uri('/foo/bar'), 1, 3, - false + false, ], 'empty uri path' => [ new Uri(''), 0, 0, - true + true, ], ]; } + public function testMatchedAllowedMethodsAreNullByDefault() + { + $result = PartialRouteResult::fromRouteMatch([], 0, 0); + $this->assertNull($result->getMatchedAllowedMethods()); + } + + public function testMatchCouldProvideListOfAllowedMethods() + { + $result = PartialRouteResult::fromRouteMatch([], 0, 0, null, ['GET']); + $this->assertEquals(['GET'], $result->getMatchedAllowedMethods()); + } + + public function testWithMatchedAllowedMethodsProducesNewInstance() + { + $result = PartialRouteResult::fromRouteMatch([], 0, 0, null, ['GET']); + $result2 = $result->withMatchedAllowedMethods(['POST']); + $this->assertNotSame($result, $result2); + $this->assertEquals(['GET'], $result->getMatchedAllowedMethods()); + $this->assertEquals(['POST'], $result2->getMatchedAllowedMethods()); + } + /** * @dataProvider provideFullPathMatchData */ From 296764a6ab500c9287b250475079cd89df855d69 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Wed, 7 Mar 2018 22:59:08 +1000 Subject: [PATCH 27/47] Make Method route provide allowed methods on match --- src/Route/Method.php | 18 ++++++++++++------ test/Route/MethodTest.php | 18 ++++++++++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Route/Method.php b/src/Route/Method.php index 2b1f72d..a83f5d6 100644 --- a/src/Route/Method.php +++ b/src/Route/Method.php @@ -11,7 +11,7 @@ use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\UriInterface; -use Zend\Router\Exception; +use Zend\Router\Exception\InvalidArgumentException; use Zend\Router\PartialRouteInterface; use Zend\Router\PartialRouteResult; use Zend\Stdlib\ArrayUtils; @@ -29,6 +29,8 @@ class Method implements PartialRouteInterface { use PartialRouteTrait; + public const OPTION_FORCE_METHOD_FAILURE = 'force_method_failure'; + /** * Verb to match. * @@ -55,7 +57,7 @@ public function __construct(string $verb, array $defaults = []) /** * Create a new method route. * - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException */ public static function factory(iterable $options = []) : self { @@ -64,7 +66,7 @@ public static function factory(iterable $options = []) : self } if (! isset($options['verb'])) { - throw new Exception\InvalidArgumentException('Missing "verb" in options array'); + throw new InvalidArgumentException('Missing "verb" in options array'); } if (! isset($options['defaults'])) { @@ -74,17 +76,21 @@ public static function factory(iterable $options = []) : self return new static($options['verb'], $options['defaults']); } + /** + * @throws InvalidArgumentException + */ public function partialMatch(Request $request, int $pathOffset = 0, array $options = []) : PartialRouteResult { if ($pathOffset < 0) { - throw new Exception\InvalidArgumentException('Path offset cannot be negative'); + throw new InvalidArgumentException('Path offset cannot be negative'); } $requestVerb = strtoupper($request->getMethod()); $matchVerbs = explode(',', strtoupper($this->verb)); $matchVerbs = array_map('trim', $matchVerbs); - if (in_array($requestVerb, $matchVerbs)) { - return PartialRouteResult::fromRouteMatch($this->defaults, $pathOffset, 0); + $forceFailure = $options[self::OPTION_FORCE_METHOD_FAILURE] ?? false; + if (! $forceFailure && in_array($requestVerb, $matchVerbs)) { + return PartialRouteResult::fromRouteMatch($this->defaults, $pathOffset, 0, null, $matchVerbs); } return PartialRouteResult::fromMethodFailure($matchVerbs, $pathOffset, 0); diff --git a/test/Route/MethodTest.php b/test/Route/MethodTest.php index f42a9cd..98d522e 100644 --- a/test/Route/MethodTest.php +++ b/test/Route/MethodTest.php @@ -40,7 +40,7 @@ public function getRouteTestDefinitions() : iterable RouteResult::fromRouteMatch([]) ) ->expectPartialMatchResult( - PartialRouteResult::fromRouteMatch([], 0, 0) + PartialRouteResult::fromRouteMatch([], 0, 0, null, ['GET']) ); yield 'match comma separated verbs' => (new RouteTestDefinition( @@ -51,7 +51,7 @@ public function getRouteTestDefinitions() : iterable RouteResult::fromRouteMatch([]) ) ->expectPartialMatchResult( - PartialRouteResult::fromRouteMatch([], 0, 0) + PartialRouteResult::fromRouteMatch([], 0, 0, null, ['GET', 'POST']) ); yield 'match comma separated verbs with whitespace' => (new RouteTestDefinition( @@ -62,7 +62,7 @@ public function getRouteTestDefinitions() : iterable RouteResult::fromRouteMatch([]) ) ->expectPartialMatchResult( - PartialRouteResult::fromRouteMatch([], 0, 0) + PartialRouteResult::fromRouteMatch([], 0, 0, null, ['GET', 'POST', 'NULL']) ); yield 'match ignores case' => (new RouteTestDefinition( @@ -73,7 +73,7 @@ public function getRouteTestDefinitions() : iterable RouteResult::fromRouteMatch([]) ) ->expectPartialMatchResult( - PartialRouteResult::fromRouteMatch([], 0, 0) + PartialRouteResult::fromRouteMatch([], 0, 0, null, ['GET']) ); yield 'no match gives list of allowed methods' => (new RouteTestDefinition( @@ -96,6 +96,16 @@ public function testAssembleSimplyReturnsPassedUri() $this->assertSame($uri, $method->assemble($uri)); } + public function testSetsAllowedMethodsOnMatch() + { + $request = new ServerRequest([], [], null, 'GET'); + $method = new Method('GET'); + $result = $method->partialMatch($request); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals(['GET'], $result->getMatchedAllowedMethods()); + } + public function testFactory() { $tester = new FactoryTester($this); From 5855c615a675321a0d3b085d3b2a684ace98f51e Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 8 Mar 2018 00:03:51 +1000 Subject: [PATCH 28/47] Refactor Part route for PSR-7 --- src/Route/Part.php | 301 ++++++++++-------- test/Route/PartTest.php | 683 +++++++++++++++++----------------------- 2 files changed, 447 insertions(+), 537 deletions(-) diff --git a/src/Route/Part.php b/src/Route/Part.php index aaa4d0d..80058cf 100644 --- a/src/Route/Part.php +++ b/src/Route/Part.php @@ -9,228 +9,259 @@ namespace Zend\Router\Route; -use ArrayObject; -use Traversable; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; use Zend\Router\Exception; -use Zend\Router\PriorityList; -use Zend\Router\RoutePluginManager; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\PartialRouteInterface; +use Zend\Router\RouteInterface; +use Zend\Router\RouteResult; +use Zend\Router\RouteStackInterface; use Zend\Router\TreeRouteStack; use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; + +use function array_diff_key; +use function array_flip; +use function array_intersect; +use function array_merge; +use function is_array; /** * Part route. */ -class Part extends TreeRouteStack implements RouteInterface +class Part implements RouteStackInterface { + use PartialRouteTrait; + /** * RouteInterface to match. * - * @var RouteInterface + * @var PartialRouteInterface */ - protected $route; + private $route; /** - * Whether the route may terminate. + * Child routes. * - * @var bool + * @var RouteStackInterface */ - protected $mayTerminate; + private $childRoutes; /** - * Child routes. + * Whether the route may terminate. * - * @var mixed + * @var bool */ - protected $childRoutes; + protected $mayTerminate; /** * Create a new part route. - * - * @param mixed $route - * @param bool $mayTerminate - * @param RoutePluginManager $routePlugins - * @param array|null $childRoutes - * @param ArrayObject|null $prototypes - * @throws Exception\InvalidArgumentException */ - public function __construct( - $route, - $mayTerminate, - RoutePluginManager $routePlugins, - array $childRoutes = null, - ArrayObject $prototypes = null - ) { - $this->routePluginManager = $routePlugins; - - if (! $route instanceof RouteInterface) { - $route = $this->routeFromArray($route); - } - - if ($route instanceof self) { - throw new Exception\InvalidArgumentException('Base route may not be a part route'); - } - - $this->route = $route; + public function __construct(PartialRouteInterface $route, RouteStackInterface $childRoutes, bool $mayTerminate) + { + $this->route = $route; + $this->childRoutes = $childRoutes; $this->mayTerminate = $mayTerminate; - $this->childRoutes = $childRoutes; - $this->prototypes = $prototypes; - $this->routes = new PriorityList(); } /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param mixed $options - * @return Part - * @throws Exception\InvalidArgumentException + * @throws InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []) : self { - if ($options instanceof Traversable) { + if (! is_array($options)) { $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'); + throw new InvalidArgumentException('Missing "route" in options array'); } - if (! isset($options['route_plugins'])) { - throw new Exception\InvalidArgumentException('Missing "route_plugins" in options array'); + if (! isset($options['child_routes']) || ! $options['child_routes']) { + $options['child_routes'] = []; } - if (! isset($options['prototypes'])) { - $options['prototypes'] = null; + if (is_array($options['child_routes'])) { + $childRoutes = new TreeRouteStack(); + $childRoutes->addRoutes($options['child_routes']); + $options['child_routes'] = $childRoutes; } if (! isset($options['may_terminate'])) { $options['may_terminate'] = false; } - if (! isset($options['child_routes']) || ! $options['child_routes']) { - $options['child_routes'] = null; - } - - if ($options['child_routes'] instanceof Traversable) { - $options['child_routes'] = ArrayUtils::iteratorToArray($options['child_routes']); - } - return new static( $options['route'], - $options['may_terminate'], - $options['route_plugins'], $options['child_routes'], - $options['prototypes'] + $options['may_terminate'] ); } /** - * match(): defined by RouteInterface interface. + * Match a given request. * - * @see \Zend\Router\RouteInterface::match() - * @param Request $request - * @param integer|null $pathOffset - * @param array $options - * @return RouteMatch|null + * @throws InvalidArgumentException on negative path offset */ - public function match(Request $request, $pathOffset = null, array $options = []) + public function match(Request $request, int $pathOffset = 0, array $options = []) : RouteResult { - if ($pathOffset === null) { - $pathOffset = 0; + if ($pathOffset < 0) { + throw new InvalidArgumentException('Path offset cannot be negative'); } + $partialResult = $this->route->partialMatch($request, $pathOffset, $options); - $match = $this->route->match($request, $pathOffset, $options); + // continue matching for method failure to allow precise method list + if ($partialResult->isFailure() && ! $partialResult->isMethodFailure()) { + return RouteResult::fromRouteFailure(); + } - if ($match !== null && method_exists($request, 'getUri')) { - if ($this->childRoutes !== null) { - $this->addRoutes($this->childRoutes); - $this->childRoutes = null; + if ($this->mayTerminate && $partialResult->isFullPathMatch($request->getUri())) { + // we get complete list of allowed methods on method failure. Child + // routes cannot expand it, so no reason to try to gather allowed + // methods for them + if ($partialResult->isMethodFailure()) { + return RouteResult::fromMethodFailure($partialResult->getAllowedMethods()); } + // We got full match, our work here is done + return RouteResult::fromRouteMatch( + $partialResult->getMatchedParams() + ); + } - $nextOffset = $pathOffset + $match->getLength(); + // pass matched params to child routes. + // Could be used for eg obtaining locale from matched parameters from parent routes. + if ($partialResult->isSuccess()) { + $options['parent_match_params'] = $options['parent_match_params'] ?? []; + $options['parent_match_params'] += $partialResult->getMatchedParams(); + } - $uri = $request->getUri(); - $pathLength = strlen($uri->getPath()); + // we continue matching only to gather allowed methods. Force + // method routes to fail + if ($partialResult->isMethodFailure()) { + $options[Method::OPTION_FORCE_METHOD_FAILURE] = true; + } - if ($this->mayTerminate && $nextOffset === $pathLength) { - return $match; - } + $nextOffset = $pathOffset + $partialResult->getMatchedPathLength(); + + $childResult = $this->childRoutes->match($request, $nextOffset, $options); + + if ($partialResult->isSuccess() && $childResult->isSuccess()) { + return $childResult->withMatchedParams( + array_merge($partialResult->getMatchedParams(), $childResult->getMatchedParams()) + ); + } - if (isset($options['translator']) - && ! isset($options['locale']) - && null !== ($locale = $match->getParam('locale', null)) - ) { - $options['locale'] = $locale; + if ($childResult->isMethodFailure() && $partialResult->isMethodFailure()) { + $methods = array_intersect( + $partialResult->getAllowedMethods(), + $childResult->getAllowedMethods() + ); + if (empty($methods)) { + return RouteResult::fromRouteFailure(); } + return RouteResult::fromMethodFailure($methods); + } + + if ($partialResult->isMethodFailure() && $childResult->isSuccess()) { + return RouteResult::fromMethodFailure( + $partialResult->getAllowedMethods() + ); + } - foreach ($this->routes as $name => $route) { - if (($subMatch = $route->match($request, $nextOffset, $options)) instanceof RouteMatch) { - if ($match->getLength() + $subMatch->getLength() + $pathOffset === $pathLength) { - return $match->merge($subMatch)->setMatchedRouteName($name); - } - } + if ($childResult->isMethodFailure()) { + $parentMethods = $partialResult->getMatchedAllowedMethods(); + $methods = $childResult->getAllowedMethods(); + if (! empty($parentMethods)) { + $methods = array_intersect( + $parentMethods, + $childResult->getAllowedMethods() + ); } + return RouteResult::fromMethodFailure($methods); } - return; + return RouteResult::fromRouteFailure(); } /** - * assemble(): Defined by RouteInterface interface. + * Assemble uri for the route. * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - * @throws Exception\RuntimeException + * @throws Exception\RuntimeException when trying to assemble part route without + * child route name, if part route can't terminate */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { - if ($this->childRoutes !== null) { - $this->addRoutes($this->childRoutes); - $this->childRoutes = null; - } - - $options['has_child'] = (isset($options['name'])); + $partOptions = $options; + $partOptions['has_child'] = isset($options['name']); + unset($partOptions['name']); - if (isset($options['translator']) && ! isset($options['locale']) && isset($params['locale'])) { - $options['locale'] = $params['locale']; - } - - $path = $this->route->assemble($params, $options); - $params = array_diff_key($params, array_flip($this->route->getAssembledParams())); + $uri = $this->route->assemble($uri, $params, $partOptions); + $params = array_diff_key($params, array_flip($this->route->getLastAssembledParams())); if (! isset($options['name'])) { if (! $this->mayTerminate) { throw new Exception\RuntimeException('Part route may not terminate'); } else { - return $path; + return $uri; } } - unset($options['has_child']); - $options['only_return_path'] = true; - $path .= parent::assemble($params, $options); + return $this->childRoutes->assemble($uri, $params, $options); + } - return $path; + /** + * Add a route to the stack. + */ + public function addRoute(string $name, RouteInterface $route, int $priority = null) : void + { + $this->childRoutes->addRoute($name, $route, $priority); } /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see RouteInterface::getAssembledParams - * @return array + * Add multiple routes to the stack. + */ + public function addRoutes(iterable $routes) : void + { + $this->childRoutes->addRoutes($routes); + } + + /** + * Remove a route from the stack. + */ + public function removeRoute(string $name) : void + { + $this->childRoutes->removeRoute($name); + } + + /** + * Remove all routes from the stack and set new ones. + */ + public function setRoutes(iterable $routes) : void + { + $this->childRoutes->setRoutes($routes); + } + + /** + * Get the added routes + */ + public function getRoutes() : array + { + return $this->childRoutes->getRoutes(); + } + + /** + * Check if a route with a specific name exists + */ + public function hasRoute(string $name) : bool + { + return $this->childRoutes->hasRoute($name); + } + + /** + * Get a route by name */ - public function getAssembledParams() + public function getRoute(string $name) : ?RouteInterface { - // Part routes may not occur as base route of other part routes, so we - // don't have to return anything here. - return []; + return $this->childRoutes->getRoute($name); } } diff --git a/test/Route/PartTest.php b/test/Route/PartTest.php index ab0ff4e..3a645d7 100644 --- a/test/Route/PartTest.php +++ b/test/Route/PartTest.php @@ -1,7 +1,7 @@ [ - '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, - ], - 'factories' => [ - Literal::class => RouteInvokableFactory::class, - Part::class => RouteInvokableFactory::class, - Segment::class => RouteInvokableFactory::class, - Wildcard::class => RouteInvokableFactory::class, - - // v2 normalized names - - 'zendmvcrouterhttpliteral' => RouteInvokableFactory::class, - 'zendmvcrouterhttppart' => RouteInvokableFactory::class, - 'zendmvcrouterhttpsegment' => RouteInvokableFactory::class, - 'zendmvcrouterhttpwildcard' => RouteInvokableFactory::class, - ], - ]); - } + use RouteTestTrait; - public static function getRoute() + public function getTestRoute() : Part { - return new Part( - [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/foo', - 'defaults' => [ - 'controller' => 'foo' - ] - ] - ], - true, - self::getRoutePlugins(), - [ - 'bar' => [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/bar', - 'defaults' => [ - 'controller' => 'bar' - ] - ] - ], - 'baz' => [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/baz' - ], + return Part::factory([ + 'route' => new Literal('/foo', ['controller' => 'foo']), + 'child_routes' => [ + 'bar' => new Literal('/bar', ['controller' => 'bar']), + 'baz' => Part::factory([ + 'route' => new Literal('/baz'), 'child_routes' => [ - 'bat' => [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/:controller' - ], - 'may_terminate' => true, - 'child_routes' => [ - 'wildcard' => [ - 'type' => Wildcard::class - ] - ] - ] - ] - ], - 'bat' => [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/bat[/:foo]', - 'defaults' => [ - 'foo' => 'bar' - ] + 'bat' => new Segment('/:controller'), ], + ]), + 'bat' => Part::factory([ + 'route' => new Segment('/bat[/:foo]', [], ['foo' => 'bar']), 'may_terminate' => true, - 'child_routes' => [ - 'literal' => [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/bar' - ] - ], - 'optional' => [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/bat[/:bar]' - ] - ], - ] - ] - ] - ); + 'child_routes' => [ + 'literal' => new Literal('/bar'), + 'optional' => new Segment('/bat[/:bar]'), + ], + ]), + ], + 'may_terminate' => true, + ]); } - public static function getRouteAlternative() + public 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' => '/' - ] - ], - ] + new Segment('/[:controller[/:action]]', [], [ + 'controller' => 'fo-fo', + 'action' => 'index', + ]), + new TreeRouteStack(), + true ); } - public static function routeProvider() + public function getRouteTestDefinitions() : iterable { - return [ - 'simple-match' => [ - self::getRoute(), - '/foo', - null, - null, - ['controller' => 'foo'] - ], - 'offset-skips-beginning' => [ - self::getRoute(), - '/bar/foo', - 4, - null, - ['controller' => 'foo'] - ], - 'simple-child-match' => [ - self::getRoute(), - '/foo/bar', - null, - 'bar', - ['controller' => 'bar'] - ], - 'offset-does-not-enable-partial-matching' => [ - self::getRoute(), - '/foo/foo', - null, - null, - null - ], - 'offset-does-not-enable-partial-matching-in-child' => [ - self::getRoute(), - '/foo/bar/baz', - null, - null, - null - ], - 'non-terminating-part-does-not-match' => [ - self::getRoute(), - '/foo/baz', - null, - null, - null - ], - 'child-of-non-terminating-part-does-match' => [ - self::getRoute(), - '/foo/baz/bat', - null, - 'baz/bat', - ['controller' => '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', - null, - 'bat', - ['foo' => 'bar'] - ], - 'optional-parameters-are-not-dropped-with-child' => [ - self::getRoute(), - '/foo/bat/bar/bar', - null, - 'bat/literal', - ['foo' => 'bar'] - ], - 'optional-parameters-not-required-in-last-part' => [ - self::getRoute(), - '/foo/bat/bar/bat', - null, - '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' - ) + $params = ['controller' => 'foo']; + yield 'simple match' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) ) - */ - ]; - } - - /** - * @dataProvider routeProvider - * @param Part $route - * @param string $path - * @param int $offset - * @param string $routeName - * @param array $params - */ - 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); - - if ($params === null) { - $this->assertNull($match); - } else { - $this->assertInstanceOf(RouteMatch::class, $match); - - if ($offset === null) { - $this->assertEquals(strlen($path), $match->getLength()); - } - - $this->assertEquals($routeName, $match->getMatchedRouteName()); - - foreach ($params as $key => $value) { - $this->assertEquals($value, $match->getParam($key)); - } - } - } - - /** - * @dataProvider routeProvider - * @param Part $route - * @param string $path - * @param int $offset - * @param string $routeName - * @param array $params - */ - public function testAssembling(Part $route, $path, $offset, $routeName, array $params = null) - { - if ($params === null) { - // Data which will not match are not tested for assembling. - return; - } - - $result = $route->assemble($params, ['name' => $routeName]); - - if ($offset !== null) { - $this->assertEquals($offset, strpos($path, $result, $offset)); - } else { - $this->assertEquals($path, $result); - } + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['controller' => 'foo']; + yield 'offset-skips-beginning' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/bar/foo') + )) + ->usePathOffset(4) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->shouldAssembleAndExpectResult(new Uri('/foo')) + ->useParamsForAssemble($params); + + $params = ['controller' => 'bar']; + yield 'simple child match' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params, 'bar') + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params) + ->useOptionsForAssemble(['name' => 'bar']); + + yield 'non terminating part does not match' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/baz') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ); + + $params = ['controller' => 'bat']; + yield 'child of non terminating part does match' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/baz/bat') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params, 'baz/bat') + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params) + ->useOptionsForAssemble(['name' => 'baz/bat']); + + $params = ['controller' => 'foo', 'foo' => 'bar']; + yield 'optional parameters are dropped without child' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/bat') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params, 'bat') + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params) + ->useOptionsForAssemble(['name' => 'bat']); + + $params = ['controller' => 'foo', 'foo' => 'bar']; + yield 'optional parameters are not dropped with child' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/bat/bar/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params, 'bat/literal') + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params) + ->useOptionsForAssemble(['name' => 'bat/literal']); + + $params = ['controller' => 'foo', 'foo' => 'bar']; + yield 'optional parameters not required in last part' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/bat/bar/bat') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params, 'bat/optional') + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params) + ->useOptionsForAssemble(['name' => 'bat/optional']); + + $params = ['controller' => 'fo-fo', 'action' => 'index']; + yield 'simple match 2' => (new RouteTestDefinition( + $this->getRouteAlternative(), + new Uri('/') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); } public function testAssembleNonTerminatedRoute() { + $uri = new Uri(); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Part route may not terminate'); - self::getRoute()->assemble([], ['name' => 'baz']); + $this->getTestRoute()->assemble($uri, [], ['name' => 'baz']); } - public function testBaseRouteMayNotBePartRoute() + public function testMethodFailureReturnsMethodFailureOnTerminatedMatch() { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Base route may not be a part route'); + $options = [ + 'route' => new Method('GET,POST'), + 'may_terminate' => true, + ]; - new Part(self::getRoute(), true, new RoutePluginManager(new ServiceManager())); + $route = Part::factory($options); + + $request = new ServerRequest([], [], new Uri('/foo'), 'PUT'); + $result = $route->match($request, 4); + $this->assertTrue($result->isMethodFailure()); + $this->assertArraySubset(['GET', 'POST'], $result->getAllowedMethods()); + $this->assertCount(2, $result->getAllowedMethods()); } - public function testNoMatchWithoutUriMethod() + public function testMethodFailureReturnsMethodFailureOnFullPathMatch() { - $route = self::getRoute(); - $request = new BaseRequest(); + $options = [ + 'route' => new Method('GET,POST'), + 'may_terminate' => true, + 'child_routes' => [ + 'foo' => new Literal('/foo'), + ], + ]; + + $route = Part::factory($options); - $this->assertNull($route->match($request)); + $request = new ServerRequest([], [], new Uri('/foo'), 'PUT'); + $result = $route->match($request, 0); + $this->assertTrue($result->isMethodFailure()); + $this->assertArraySubset(['GET', 'POST'], $result->getAllowedMethods()); + $this->assertCount(2, $result->getAllowedMethods()); } - public function testGetAssembledParams() + public function testMethodFailureReturnsFailureIfChildRoutesFail() { - $route = self::getRoute(); - $route->assemble(['controller' => 'foo'], ['name' => 'baz/bat']); + $options = [ + 'route' => new Method('GET,POST'), + 'may_terminate' => true, + 'child_routes' => [ + 'foo' => new Literal('/foo'), + ], + ]; + $route = Part::factory($options); - $this->assertEquals([], $route->getAssembledParams()); + $request = new ServerRequest([], [], new Uri('/bar'), 'PUT'); + $result = $route->match($request, 0); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); } - public function testFactory() + public function testMethodFailureReturnsMethodIntersectionBetweenPartialAndChildRoutes() { - $tester = new FactoryTester($this); - $tester->testFactory( - Part::class, - [ - 'route' => 'Missing "route" in options array', - 'route_plugins' => 'Missing "route_plugins" in options array' + $options = [ + 'route' => new Method('GET,POST'), + 'may_terminate' => true, + 'child_routes' => [ + 'foo' => Part::factory([ + 'route' => new Literal('/foo'), + 'child_routes' => [ + 'verb' => new Method('POST,DELETE'), + ], + ]), ], - [ - 'route' => new Literal('/foo'), - 'route_plugins' => self::getRoutePlugins(), - ] - ); + ]; + + $route = Part::factory($options); + + $request = new ServerRequest([], [], new Uri('/foo'), 'PUT'); + $result = $route->match($request, 0); + $this->assertTrue($result->isMethodFailure()); + $this->assertEquals(['POST'], $result->getAllowedMethods()); } - /** - * @group ZF2-105 - */ - public function testFactoryShouldAcceptTraversableChildRoutes() + public function testMethodFailureWithChildMethodsNotIntersectingIsAFailure() { - $children = new ArrayObject([ - 'create' => [ - 'type' => 'Literal', - 'options' => [ - 'route' => 'create', - 'defaults' => [ - 'controller' => 'user-admin', - 'action' => 'edit', - ], - ], - ], - ]); $options = [ - 'route' => [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/admin/users', - 'defaults' => [ - 'controller' => 'Admin\UserController', - 'action' => 'index', + 'route' => new Method('GET,POST'), + 'may_terminate' => true, + 'child_routes' => [ + 'foo' => Part::factory([ + 'route' => new Literal('/foo'), + 'child_routes' => [ + 'verb' => new Method('DELETE,OPTIONS'), ], - ], + ]), ], - 'route_plugins' => self::getRoutePlugins(), - 'may_terminate' => true, - 'child_routes' => $children, ]; $route = Part::factory($options); - $this->assertInstanceOf(Part::class, $route); + + $request = new ServerRequest([], [], new Uri('/foo'), 'PUT'); + $result = $route->match($request, 0); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); } - /** - * @group 3711 - */ - public function testPartRouteMarkedAsMayTerminateCanMatchWhenQueryStringPresent() + public function testChildMethodFailureWithParentPartSuccessReturnsFullListOfMethods() { $options = [ - 'route' => [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/resource', - 'defaults' => [ - 'controller' => 'ResourceController', - 'action' => 'resource', + 'route' => new Method('GET,POST,DELETE'), + 'may_terminate' => true, + 'child_routes' => [ + 'foo' => Part::factory([ + 'route' => new Literal('/foo'), + 'child_routes' => [ + 'verb' => new Method('POST,DELETE,OPTIONS'), ], - ], + ]), ], - 'route_plugins' => self::getRoutePlugins(), + ]; + + $route = Part::factory($options); + + $request = new ServerRequest([], [], new Uri('/foo'), 'GET'); + $result = $route->match($request, 0); + $this->assertTrue($result->isMethodFailure()); + $this->assertEquals(['POST', 'DELETE'], $result->getAllowedMethods()); + } + + public function testParentMethodFailureWithChildSuccessReturnsFullListOfMethods() + { + $options = [ + 'route' => new Method('GET,POST,DELETE'), 'may_terminate' => true, - 'child_routes' => [ - 'child' => [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/child', - 'defaults' => [ - 'action' => 'child', - ], + 'child_routes' => [ + 'foo' => Part::factory([ + 'route' => new Literal('/foo'), + 'child_routes' => [ + 'verb' => new Method('DELETE,OPTIONS'), ], - ], + ]), ], ]; $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(); - - $match = $route->match($request); - $this->assertInstanceOf(\Zend\Router\RouteMatch::class, $match); - $this->assertEquals('resource', $match->getParam('action')); + + $request = new ServerRequest([], [], new Uri('/foo'), 'OPTIONS'); + $result = $route->match($request, 0); + $this->assertTrue($result->isMethodFailure()); + $this->assertEquals(['DELETE'], $result->getAllowedMethods()); + } + + public function testFactory() + { + $tester = new FactoryTester($this); + $tester->testFactory( + Part::class, + [ + 'route' => 'Missing "route" in options array', + ], + [ + 'route' => new Literal('/foo'), + ] + ); } /** * @group 3711 */ - public function testPartRouteMarkedAsMayTerminateButWithQueryRouteChildWillMatchChildRoute() + public function testPartRouteMarkedAsMayTerminateCanMatchWhenQueryStringPresent() { $options = [ - 'route' => [ - 'type' => Literal::class, - 'options' => [ - 'route' => '/resource', - 'defaults' => [ - 'controller' => 'ResourceController', - 'action' => 'resource', - ], - ], - ], - 'route_plugins' => self::getRoutePlugins(), + 'route' => new Literal('/resource', ['controller' => 'ResourceController', 'action' => 'resource']), 'may_terminate' => true, + 'child_routes' => [ + 'child' => new Literal('/child'), + ], ]; $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(); - - /* - $match = $route->match($request); - $this->assertInstanceOf(\Zend\Router\RouteMatch::class, $match); - $this->assertEquals('string', $match->getParam('query')); - */ + $request = new ServerRequest([], [], new Uri('http://example.com/resource?foo=bar')); + $request = $request->withQueryParams(['foo' => 'bar']); + + $result = $route->match($request); + $this->assertTrue($result->isSuccess()); + $this->assertEquals('resource', $result->getMatchedParams()['action']); + } + + public function testRejectsNegativePathOffset() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Path offset cannot be negative'); + $partial = $this->prophesize(PartialRouteInterface::class); + $request = $this->prophesize(ServerRequestInterface::class); + $route = new Part($partial->reveal(), new TreeRouteStack(), false); + $route->match($request->reveal(), -1); } } From a5e4f70ee597d985678adf595faa8b9c3fd396b4 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 8 Mar 2018 04:13:18 +1000 Subject: [PATCH 29/47] Refactor Chain route for PSR-7 --- src/Route/Chain.php | 218 ++++++++++++------------ test/Route/ChainTest.php | 360 ++++++++++++++++++++++----------------- 2 files changed, 317 insertions(+), 261 deletions(-) diff --git a/src/Route/Chain.php b/src/Route/Chain.php index c34f988..dba9795 100644 --- a/src/Route/Chain.php +++ b/src/Route/Chain.php @@ -9,26 +9,31 @@ namespace Zend\Router\Route; -use ArrayObject; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; use Traversable; -use Zend\Router\Exception; -use Zend\Router\PriorityList; -use Zend\Router\RoutePluginManager; -use Zend\Router\TreeRouteStack; +use Zend\Router\Exception\InvalidArgumentException; +use Zend\Router\PartialRouteInterface; +use Zend\Router\PartialRouteResult; +use Zend\Router\RouteInterface; +use Zend\Router\SimpleRouteStack; use Zend\Stdlib\ArrayUtils; -use Zend\Stdlib\RequestInterface as Request; + +use function array_diff_key; +use function array_flip; +use function array_intersect; +use function array_merge; +use function array_reverse; +use function end; +use function is_array; +use function key; /** * Chain route. */ -class Chain extends TreeRouteStack implements RouteInterface +class Chain extends SimpleRouteStack implements PartialRouteInterface { - /** - * Chain routes. - * - * @var array - */ - protected $chainRoutes; + use PartialRouteTrait; /** * List of assembled parameters. @@ -38,157 +43,156 @@ class Chain extends TreeRouteStack implements RouteInterface protected $assembledParams = []; /** - * Create a new part route. - * - * @param array $routes - * @param RoutePluginManager $routePlugins - * @param ArrayObject|null $prototypes + * Create a new chain route. */ - public function __construct(array $routes, RoutePluginManager $routePlugins, ArrayObject $prototypes = null) + public function __construct(array $routes) { - $this->chainRoutes = array_reverse($routes); - $this->routePluginManager = $routePlugins; - $this->routes = new PriorityList(); - $this->prototypes = $prototypes; + parent::__construct(); + $routes = array_reverse($routes, true); + $this->addRoutes($routes); } /** - * factory(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::factory() - * @param mixed $options - * @throws Exception\InvalidArgumentException - * @return Part + * @throws InvalidArgumentException */ - public static function factory($options = []) + public static function factory(iterable $options = []) : self { - if ($options instanceof Traversable) { + if (! is_array($options)) { $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; + throw new InvalidArgumentException('Missing "routes" in options array'); } 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'] = ArrayUtils::iteratorToArray($options['routes']); } return new static( - $options['routes'], - $options['route_plugins'], - $options['prototypes'] + $options['routes'] ); } /** - * match(): defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::match() - * @param Request $request - * @param int|null $pathOffset - * @param array $options - * @return RouteMatch|null + * @throws InvalidArgumentException */ - public function match(Request $request, $pathOffset = null, array $options = []) + public function addRoute(string $name, RouteInterface $route, int $priority = null) : void { - if (! method_exists($request, 'getUri')) { - return; + if (! $route instanceof PartialRouteInterface) { + throw new InvalidArgumentException('Chain route can only chain partial routes'); } + parent::addRoute($name, $route, $priority); + } - if ($pathOffset === null) { - $mustTerminate = true; - $pathOffset = 0; - } else { - $mustTerminate = false; + /** + * @throws InvalidArgumentException + */ + public function partialMatch(Request $request, int $pathOffset = 0, array $options = []) : PartialRouteResult + { + if ($pathOffset < 0) { + throw new InvalidArgumentException('Path offset cannot be negative'); } - if ($this->chainRoutes !== null) { - $this->addRoutes($this->chainRoutes); - $this->chainRoutes = null; + $nextPathOffset = $pathOffset; + $methodFailure = false; + $allowedMethods = null; + $matchedParams = []; + + if ($this->routes->count() === 0) { + return PartialRouteResult::fromRouteFailure(); } - $match = new RouteMatch([]); - $uri = $request->getUri(); - $pathLength = strlen($uri->getPath()); + foreach ($this->getRoutes() as $route) { + /** @var PartialRouteInterface $route */ + $result = $route->partialMatch($request, $nextPathOffset, $options); + + if ($result->isFailure() && ! $result->isMethodFailure()) { + return $result; + } + + if ($result->isMethodFailure()) { + $methodFailure = true; + // make all following method routes fail, needed for allowed + // methods gathering by Part route even tho it should not + // be normally allowed to be chained + $options[Method::OPTION_FORCE_METHOD_FAILURE] = true; + + $allowedMethods = $allowedMethods ?? $result->getAllowedMethods(); + $allowedMethods = array_intersect( + $allowedMethods, + $result->getAllowedMethods() + ); + } - foreach ($this->routes as $route) { - $subMatch = $route->match($request, $pathOffset, $options); + if ($result->isSuccess()) { + $matchedParams = array_merge($matchedParams, $result->getMatchedParams()); - if ($subMatch === null) { - return; + $options['parent_match_params'] = $options['parent_match_params'] ?? []; + $options['parent_match_params'] += $matchedParams; + + $methods = $result->getMatchedAllowedMethods(); + if (! empty($methods)) { + $allowedMethods = $allowedMethods ?? $methods; + $allowedMethods = array_intersect($allowedMethods, $methods); + } } - $match->merge($subMatch); - $pathOffset += $subMatch->getLength(); + $nextPathOffset += $result->getMatchedPathLength(); } - if ($mustTerminate && $pathOffset !== $pathLength) { - return; + $matchedLength = $nextPathOffset - $pathOffset; + if ($methodFailure) { + if (empty($allowedMethods)) { + return PartialRouteResult::fromRouteFailure(); + } + return PartialRouteResult::fromMethodFailure($allowedMethods, $pathOffset, $matchedLength); } - return $match; + // explicitly discarding chained route names if any + return PartialRouteResult::fromRouteMatch( + $matchedParams, + $pathOffset, + $matchedLength, + null, + $allowedMethods + ); } - /** - * assemble(): Defined by RouteInterface interface. - * - * @see \Zend\Router\RouteInterface::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = [], array $options = []) + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface { - if ($this->chainRoutes !== null) { - $this->addRoutes($this->chainRoutes); - $this->chainRoutes = null; - } - $this->assembledParams = []; $routes = ArrayUtils::iteratorToArray($this->routes); - end($routes); $lastRouteKey = key($routes); - $path = ''; foreach ($routes as $key => $route) { + /** @var PartialRouteInterface $route */ $chainOptions = $options; - $hasChild = isset($options['has_child']) ? $options['has_child'] : false; + $hasChild = isset($options['has_child']) ? $options['has_child'] : false; - $chainOptions['has_child'] = ($hasChild || $key !== $lastRouteKey); + $chainOptions['has_child'] = $hasChild || $key !== $lastRouteKey; - $path .= $route->assemble($params, $chainOptions); - $params = array_diff_key($params, array_flip($route->getAssembledParams())); + $uri = $route->assemble($uri, $params, $chainOptions); + $params = array_diff_key($params, array_flip($route->getLastAssembledParams())); - $this->assembledParams += $route->getAssembledParams(); + $this->assembledParams = array_merge($this->assembledParams, $route->getLastAssembledParams()); } - return $path; + return $uri; + } + + public function getLastAssembledParams() : array + { + return $this->assembledParams; } /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see RouteInterface::getAssembledParams - * @return array + * @deprecated */ - public function getAssembledParams() + public function getAssembledParams() : array { - return $this->assembledParams; + return $this->getLastAssembledParams(); } } diff --git a/test/Route/ChainTest.php b/test/Route/ChainTest.php index f2498d5..9a052a1 100644 --- a/test/Route/ChainTest.php +++ b/test/Route/ChainTest.php @@ -1,7 +1,7 @@ new Segment('/:controller', [], ['controller' => 'foo']), + 'bar' => new Segment('/:bar', [], ['bar' => 'bar']), + ]); + } - return new Chain( - [ - [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/:controller', - 'defaults' => [ - 'controller' => 'foo', - ], - ], - ], - [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/:bar', - 'defaults' => [ - 'bar' => 'bar', - ], - ], - ], - [ - 'type' => Wildcard::class, - ], - ], - $routePlugins - ); + public function getRouteWithOptionalParam() : Chain + { + return new Chain([ + 'foo' => new Segment('/:controller', [], ['controller' => 'foo']), + 'bar' => new Segment('[/:bar]', [], ['bar' => 'bar']), + ]); } - public static function getRouteWithOptionalParam() + public function getRouteTestDefinitions() : iterable { - $routePlugins = new RoutePluginManager(new ServiceManager()); + $params = ['controller' => 'foo', 'bar' => 'bar']; + yield 'simple match' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 8) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); - return new Chain( - [ - [ - 'type' => Segment::class, - 'options' => [ - 'route' => '/:controller', - 'defaults' => [ - 'controller' => 'foo', - ], - ], - ], - [ - 'type' => Segment::class, - 'options' => [ - 'route' => '[/:bar]', - 'defaults' => [ - 'bar' => 'bar', - ], - ], - ], - ], - $routePlugins - ); + $params = ['controller' => 'foo', 'bar' => 'bar']; + yield 'offset skips beginning' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/baz/foo/bar') + )) + ->usePathOffset(4) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 4, 8) + ) + ->shouldAssembleAndExpectResult(new Uri('/foo/bar')) + ->useParamsForAssemble($params); + + $params = ['controller' => 'foo', 'bar' => 'baz']; + yield 'parameters are used only once' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 8) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['controller' => 'foo', 'bar' => 'baz']; + yield 'optional parameter' => (new RouteTestDefinition( + $this->getRouteWithOptionalParam(), + new Uri('/foo/baz') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 8) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['controller' => 'foo', 'bar' => 'bar']; + yield 'optional parameter empty' => (new RouteTestDefinition( + $this->getRouteWithOptionalParam(), + new Uri('/foo') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 4) + ) + ->shouldAssembleAndExpectResultSameAsUriForMatching() + ->useParamsForAssemble($params); + + $params = ['controller' => 'foo', 'bar' => 'bar']; + yield 'partial match' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/bar/baz') + )) + ->expectMatchResult( + RouteResult::fromRouteFailure() + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 8) + ) + ->shouldAssembleAndExpectResult(new Uri('/foo/bar')) + ->useParamsForAssemble($params); + + $params = ['controller' => 'foo', 'bar' => 'bar']; + yield 'assemble appends path' => (new RouteTestDefinition( + $this->getTestRoute(), + new Uri('/foo/bar') + )) + ->expectMatchResult( + RouteResult::fromRouteMatch($params) + ) + ->expectPartialMatchResult( + PartialRouteResult::fromRouteMatch($params, 0, 8) + ) + ->shouldAssembleAndExpectResult(new Uri('/prefixed/foo/bar')) + ->useUriForAssemble(new Uri('/prefixed')) + ->useParamsForAssemble($params); } - public static function routeProvider() + public function testOnlyPartialRoutesAreAllowed() { - return [ - 'simple-match' => [ - self::getRoute(), - '/foo/bar', - null, - [ - 'controller' => 'foo', - 'bar' => 'bar', - ], - ], - 'offset-skips-beginning' => [ - self::getRoute(), - '/baz/foo/bar', - 4, - [ - 'controller' => 'foo', - 'bar' => 'bar', - ], - ], - 'parameters-are-used-only-once' => [ - self::getRoute(), - '/foo/baz', - null, - [ - 'controller' => 'foo', - 'bar' => 'baz', - ], - ], - 'optional-parameter' => [ - self::getRouteWithOptionalParam(), - '/foo/baz', - null, - [ - 'controller' => 'foo', - 'bar' => 'baz', - ], - ], - 'optional-parameter-empty' => [ - self::getRouteWithOptionalParam(), - '/foo', - null, - [ - 'controller' => 'foo', - 'bar' => 'bar', - ], - ], - ]; + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Chain route can only chain partial routes'); + new Chain([ + 'foo' => $this->prophesize(RouteInterface::class)->reveal(), + ]); + } + + public function testAddRouteAllowsOnlyPartialRoute() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Chain route can only chain partial routes'); + (new Chain([]))->addRoute('foo', $this->prophesize(RouteInterface::class)->reveal()); + } + + public function testMethodFailureReturnsMethodFailureResult() + { + $request = new ServerRequest([], [], new Uri('/foo'), 'PUT'); + $route = new Chain([ + 'method' => new Method('GET,POST'), + 'literal' => new Literal('/foo'), + ]); + $result = $route->match($request); + $this->assertTrue($result->isMethodFailure()); + $this->assertArraySubset(['GET', 'POST'], $result->getAllowedMethods()); + $this->assertCount(2, $result->getAllowedMethods()); + } + + public function testMethodFailureReturnsMethodIntersection() + { + $request = new ServerRequest([], [], new Uri('/foo'), 'PUT'); + $route = new Chain([ + 'method1' => new Method('GET,POST'), + 'method2' => new Method('POST,DELETE'), + 'literal' => new Literal('/foo'), + ]); + $result = $route->match($request); + $this->assertTrue($result->isMethodFailure()); + $this->assertEquals(['POST'], $result->getAllowedMethods()); + } + + public function testMethodFailureWithMethodsNotIntersectingIsAFailure() + { + $request = new ServerRequest([], [], new Uri('/foo'), 'PUT'); + $route = new Chain([ + 'method1' => new Method('GET,POST'), + 'method2' => new Method('PUT,DELETE'), + 'literal' => new Literal('/foo'), + ]); + $result = $route->match($request); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); } - /** - * @dataProvider routeProvider - * @param Chain $route - * @param string $path - * @param int $offset - * @param array $params - */ - public function testMatching(Chain $route, $path, $offset, array $params = null) + public function testMethodFailureReturnsFailureIfOtherRoutesFail() { - $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)); - } - } + $request = new ServerRequest([], [], new Uri('/foo'), 'PUT'); + $route = new Chain([ + 'method1' => new Method('GET,POST'), + 'literal' => new Literal('/bar'), + ]); + $result = $route->match($request); + $this->assertTrue($result->isFailure()); + $this->assertFalse($result->isMethodFailure()); } - /** - * @dataProvider routeProvider - * @param Chain $route - * @param string $path - * @param int $offset - * @param array $params - */ - public function testAssembling(Chain $route, $path, $offset, array $params = null) + public function testGetAssembledParams() { - if ($params === null) { - // Data which will not match are not tested for assembling. - return; - } - - $result = $route->assemble($params); - - if ($offset !== null) { - $this->assertEquals($offset, strpos($path, $result, $offset)); - } else { - $this->assertEquals($path, $result); - } + $uri = new Uri(); + + /** @var Chain $route */ + $route = $this->getTestRoute(); + $route->assemble($uri, ['controller' => 'foo', 'bar' => 'baz', 'bat' => 'bat']); + + $this->assertEquals(['controller', 'bar'], $route->getLastAssembledParams()); + $this->assertEquals($route->getLastAssembledParams(), $route->getAssembledParams()); } public function testFactory() @@ -193,12 +238,19 @@ public function testFactory() $tester->testFactory( Chain::class, [ - 'routes' => 'Missing "routes" in options array', - 'route_plugins' => 'Missing "route_plugins" in options array', + 'routes' => 'Missing "routes" in options array', + ], + [ + 'routes' => [], + ] + ); + $tester->testFactory( + Chain::class, + [ + 'routes' => 'Missing "routes" in options array', ], [ - 'routes' => [], - 'route_plugins' => new RoutePluginManager(new ServiceManager()), + 'routes' => new ArrayObject(), ] ); } From e4a8cc7e106862f85439f2e85cbdbc08a343760a Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 8 Mar 2018 04:32:20 +1000 Subject: [PATCH 30/47] Remove now redundant HttpRouterFactory --- src/Route/HttpRouterFactory.php | 58 ---------------------------- test/Route/HttpRouterFactoryTest.php | 30 -------------- 2 files changed, 88 deletions(-) delete mode 100644 src/Route/HttpRouterFactory.php delete mode 100644 test/Route/HttpRouterFactoryTest.php diff --git a/src/Route/HttpRouterFactory.php b/src/Route/HttpRouterFactory.php deleted file mode 100644 index e4ef606..0000000 --- a/src/Route/HttpRouterFactory.php +++ /dev/null @@ -1,58 +0,0 @@ -has('config') ? $container->get('config') : []; - - // Defaults - $class = TreeRouteStack::class; - $config = isset($config['router']) ? $config['router'] : []; - - return $this->createRouter($class, $config, $container); - } - - /** - * Create and return RouteStackInterface instance - * - * For use with zend-servicemanager v2; proxies to __invoke(). - * - * @param ServiceLocatorInterface $container - * @return RouteStackInterface - */ - public function createService(ServiceLocatorInterface $container) - { - return $this($container, RouteStackInterface::class); - } -} diff --git a/test/Route/HttpRouterFactoryTest.php b/test/Route/HttpRouterFactoryTest.php deleted file mode 100644 index b08e890..0000000 --- a/test/Route/HttpRouterFactoryTest.php +++ /dev/null @@ -1,30 +0,0 @@ -defaultServiceConfig = [ - 'factories' => [ - 'RoutePluginManager' => function ($services) { - return new RoutePluginManager($services); - }, - ], - ]; - - $this->factory = new HttpRouterFactory(); - } -} From 01e3aad29e8374643244872a4a3c50c01d1853b2 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 8 Mar 2018 17:04:43 +1000 Subject: [PATCH 31/47] Add route definitions to RoutePluginManager and update factory --- src/ConfigProvider.php | 2 + src/Container/RoutePluginManagerFactory.php | 33 ++++ src/RoutePluginManager.php | 169 +++++------------- src/RoutePluginManagerFactory.php | 44 ----- .../RoutePluginManagerFactoryTest.php | 28 ++- test/RoutePluginManagerTest.php | 10 +- 6 files changed, 96 insertions(+), 190 deletions(-) create mode 100644 src/Container/RoutePluginManagerFactory.php delete mode 100644 src/RoutePluginManagerFactory.php rename test/{ => Container}/RoutePluginManagerFactoryTest.php (65%) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index a99d6e9..6a0a981 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -9,6 +9,8 @@ namespace Zend\Router; +use Zend\Router\Container\RoutePluginManagerFactory; + /** * Provide base configuration for using the component. * diff --git a/src/Container/RoutePluginManagerFactory.php b/src/Container/RoutePluginManagerFactory.php new file mode 100644 index 0000000..2fed528 --- /dev/null +++ b/src/Container/RoutePluginManagerFactory.php @@ -0,0 +1,33 @@ +getRoutesConfig($container); + return new RoutePluginManager($container, $options); + } + + public function getRoutesConfig(ContainerInterface $container) : array + { + return $container->get('config')[RoutePluginManager::class] ?? []; + } +} diff --git a/src/RoutePluginManager.php b/src/RoutePluginManager.php index 06c809f..f61990b 100644 --- a/src/RoutePluginManager.php +++ b/src/RoutePluginManager.php @@ -1,7 +1,7 @@ Route\Chain::class, + 'Chain' => Route\Chain::class, + 'hostname' => Route\Hostname::class, + 'Hostname' => Route\Hostname::class, + 'literal' => Route\Literal::class, + 'Literal' => Route\Literal::class, + 'method' => Route\Method::class, + 'Method' => Route\Method::class, + 'part' => Route\Part::class, + 'Part' => Route\Part::class, + 'regex' => Route\Regex::class, + 'Regex' => Route\Regex::class, + 'scheme' => Route\Scheme::class, + 'Scheme' => Route\Scheme::class, + 'segment' => Route\Segment::class, + 'Segment' => Route\Segment::class, + 'Zend\Router\Http\Chain' => Route\Chain::class, + 'Zend\Router\Http\Hostname' => Route\Hostname::class, + 'Zend\Router\Http\Literal' => Route\Literal::class, + 'Zend\Router\Http\Method' => Route\Method::class, + 'Zend\Router\Http\Part' => Route\Part::class, + 'Zend\Router\Http\Regex' => Route\Regex::class, + 'Zend\Router\Http\Scheme' => Route\Scheme::class, + 'Zend\Router\Http\Segment' => Route\Segment::class, + ]; + + /** + * @var array */ - protected $sharedByDefault = false; + protected $factories = [ + Route\Chain::class => RouteInvokableFactory::class, + Route\Hostname::class => RouteInvokableFactory::class, + Route\Literal::class => RouteInvokableFactory::class, + Route\Method::class => RouteInvokableFactory::class, + Route\Part::class => RouteInvokableFactory::class, + Route\Regex::class => RouteInvokableFactory::class, + Route\Scheme::class => RouteInvokableFactory::class, + Route\Segment::class => RouteInvokableFactory::class, + ]; /** * Constructor @@ -53,125 +89,10 @@ class RoutePluginManager extends AbstractPluginManager * abstract factory. * * @param ContainerInterface|\Zend\ServiceManager\ConfigInterface $configOrContainerInstance - * @param array $v3config */ - public function __construct($configOrContainerInstance, array $v3config = []) + public function __construct($configOrContainerInstance, array $config = []) { $this->addAbstractFactory(RouteInvokableFactory::class); - parent::__construct($configOrContainerInstance, $v3config); - } - - /** - * Validate a route plugin. (v2) - * - * @param object $plugin - * @throws InvalidServiceException - */ - public function validate($plugin) - { - if (! $plugin instanceof $this->instanceOf) { - throw new InvalidServiceException(sprintf( - 'Plugin of type %s is invalid; must implement %s', - (is_object($plugin) ? get_class($plugin) : gettype($plugin)), - RouteInterface::class - )); - } - } - - /** - * Validate a route plugin. (v2) - * - * @param object $plugin - * @throws Exception\RuntimeException - */ - public function validatePlugin($plugin) - { - try { - $this->validate($plugin); - } catch (InvalidServiceException $e) { - throw new Exception\RuntimeException( - $e->getMessage(), - $e->getCode(), - $e - ); - } - } - - /** - * Pre-process configuration. (v3) - * - * Checks for invokables, and, if found, maps them to the - * component-specific RouteInvokableFactory; removes the invokables entry - * before passing to the parent. - * - * @param array $config - * @return void - */ - public function configure(array $config) - { - if (isset($config['invokables']) && ! empty($config['invokables'])) { - $aliases = $this->createAliasesForInvokables($config['invokables']); - $factories = $this->createFactoriesForInvokables($config['invokables']); - - if (! empty($aliases)) { - $config['aliases'] = isset($config['aliases']) - ? array_merge($config['aliases'], $aliases) - : $aliases; - } - - $config['factories'] = isset($config['factories']) - ? array_merge($config['factories'], $factories) - : $factories; - - unset($config['invokables']); - } - - parent::configure($config); - } - - /** - * 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) - { - $aliases = []; - foreach ($invokables as $name => $class) { - if ($name === $class) { - continue; - } - $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. - * - * @param array $invokables - * @return array - */ - protected function createFactoriesForInvokables(array $invokables) - { - $factories = []; - foreach ($invokables as $name => $class) { - if ($name === $class) { - $factories[$name] = RouteInvokableFactory::class; - continue; - } - - $factories[$class] = RouteInvokableFactory::class; - } - return $factories; + parent::__construct($configOrContainerInstance, $config); } } diff --git a/src/RoutePluginManagerFactory.php b/src/RoutePluginManagerFactory.php deleted file mode 100644 index 99a49d4..0000000 --- a/src/RoutePluginManagerFactory.php +++ /dev/null @@ -1,44 +0,0 @@ -assertInstanceOf(RoutePluginManager::class, $plugins); } - public function testCreateServiceReturnsAPluginManager() - { - $container = $this->prophesize(ServiceLocatorInterface::class); - $container->willImplement(ContainerInterface::class); - - $plugins = $this->factory->createService($container->reveal()); - $this->assertInstanceOf(RoutePluginManager::class, $plugins); - } - public function testInvocationCanProvideOptionsToThePluginManager() { - $options = ['factories' => [ - 'TestRoute' => function ($container) { - return $this->prophesize(RouteInterface::class)->reveal(); - }, - ]]; + $options = [ + 'factories' => [ + 'TestRoute' => function ($container) { + return $this->prophesize(RouteInterface::class)->reveal(); + }, + ], + ]; $plugins = $this->factory->__invoke( $this->container->reveal(), RoutePluginManager::class, diff --git a/test/RoutePluginManagerTest.php b/test/RoutePluginManagerTest.php index b222b36..d918145 100644 --- a/test/RoutePluginManagerTest.php +++ b/test/RoutePluginManagerTest.php @@ -1,7 +1,7 @@ [ - 'DummyRoute' => TestAsset\DummyRoute::class, - ]]); + $routes = new RoutePluginManager(new ServiceManager(), [ + 'aliases' => [ + 'DummyRoute' => TestAsset\DummyRoute::class, + ], + ]); $route = $routes->get('DummyRoute'); $this->assertInstanceOf(TestAsset\DummyRoute::class, $route); From 6b16272e4d3b2f0b5231f6dd81cefe8e2b3bb6a9 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Tue, 13 Mar 2018 03:55:34 +1000 Subject: [PATCH 32/47] Initial implementation for route config factory --- src/RouteConfigFactory.php | 167 +++++++++++++++++++++++ test/RouteConfigFactoryTest.php | 226 ++++++++++++++++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 src/RouteConfigFactory.php create mode 100644 test/RouteConfigFactoryTest.php diff --git a/src/RouteConfigFactory.php b/src/RouteConfigFactory.php new file mode 100644 index 0000000..89f1ba1 --- /dev/null +++ b/src/RouteConfigFactory.php @@ -0,0 +1,167 @@ +routes = $routes; + } + + /** + * Creates route or route tree from the provided spec + * + * @param array|string|RouteInterface $spec + * @throws RuntimeException + * @throws InvalidArgumentException + */ + public function routeFromSpec($spec) : RouteInterface + { + if ($spec instanceof RouteInterface) { + return $spec; + } + + if (is_string($spec)) { + if (null === ($route = $this->getPrototype($spec))) { + throw new RuntimeException(sprintf('Could not find prototype with name %s', $spec)); + } + + return $route; + } + + if (! is_array($spec)) { + throw new InvalidArgumentException('Route definition must be an array'); + } + + if (isset($spec['chain_routes'])) { + $route = $this->createChainFromSpec($spec); + } else { + if (! isset($spec['type'])) { + throw new InvalidArgumentException('Missing "type" option'); + } + + if (! isset($spec['options'])) { + $spec['options'] = []; + } + + $route = $this->routes->build($spec['type'], $spec['options']); + + if (isset($spec['priority'])) { + $route->priority = $spec['priority']; + } + } + + if (isset($spec['child_routes'])) { + $route = $this->createPartFromSpec($spec, $route); + } + + return $route; + } + + /** + * Returns defined prototype route + */ + public function getPrototype(string $name) : ?RouteInterface + { + return $this->prototypes[$name] ?? null; + } + + /** + * Defines prototype route to be re-used when creating routes from spec + */ + public function addPrototype(string $name, RouteInterface $route) : void + { + $this->prototypes[$name] = $route; + } + + /** + * Wraps route in spec with Chain route, adds chain_routes to chain + * + * @throws InvalidArgumentException + */ + private function createChainFromSpec(array $specs) : RouteInterface + { + if (! is_array($specs['chain_routes'])) { + throw new InvalidArgumentException('Chain routes must be an array'); + } + + $chainRoutesSpec = $specs['chain_routes']; + + $route = $specs; + unset($route['chain_routes']); + unset($route['child_routes']); + + array_unshift($chainRoutesSpec, $route); + + $chainRoutes = []; + foreach ($chainRoutesSpec as $name => $routeSpec) { + if (is_numeric($name)) { + $name = sprintf('__chained_route_no_name_%d', self::$chainedIndex++); + } + $chainRoutes[$name] = $this->routeFromSpec($routeSpec); + } + + $options = [ + 'routes' => $chainRoutes, + ]; + + return $this->routes->build('chain', $options); + } + + /** + * Wraps route in spec with Part route and adds child_routes to Part + */ + private function createPartFromSpec(array $specs, RouteInterface $route) : RouteInterface + { + $childRoutes = []; + foreach ($specs['child_routes'] as $name => $childSpec) { + $childRoutes[$name] = $this->routeFromSpec($childSpec); + } + $options = [ + 'route' => $route, + 'may_terminate' => $specs['may_terminate'] ?? false, + 'child_routes' => $childRoutes, + ]; + + $priority = isset($route->priority) ? $route->priority : null; + + $route = $this->routes->build('part', $options); + if (isset($priority)) { + $route->priority = $priority; + } + + return $route; + } +} diff --git a/test/RouteConfigFactoryTest.php b/test/RouteConfigFactoryTest.php new file mode 100644 index 0000000..1fb376e --- /dev/null +++ b/test/RouteConfigFactoryTest.php @@ -0,0 +1,226 @@ +routes = new RoutePluginManager(new ServiceManager()); + $this->factory = new RouteConfigFactory($this->routes); + } + + public function testCreateFromArray() + { + $spec = [ + 'type' => 'TestRoute', + 'options' => [ + 'foo' => 'bar', + ], + ]; + $route = $this->prophesize(RouteInterface::class); + + $routeFactory = $this->prophesize(FactoryInterface::class); + $routeFactory->__invoke(Argument::any(), 'TestRoute', $spec['options']) + ->shouldBeCalled() + ->willReturn($route->reveal()); + + $this->routes->setFactory('TestRoute', $routeFactory->reveal()); + + $returnedRoute = $this->factory->routeFromSpec($spec); + $this->assertSame($route->reveal(), $returnedRoute); + } + + public function testCreateFromRouteInstanceReturnsSameInstance() + { + $route = $this->prophesize(RouteInterface::class); + $returnedRoute = $this->factory->routeFromSpec($route->reveal()); + $this->assertSame($route->reveal(), $returnedRoute); + } + + public function testCreateFromNonArraySpecShouldThrow() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Route definition must be an array'); + $this->factory->routeFromSpec(123); + } + + public function testCreateRouteWithChained() + { + $spec = [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/foo', + ], + 'chain_routes' => [ + 'chained' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/bar', + ], + ], + ], + ]; + + $chainRoute = $this->factory->routeFromSpec($spec); + $this->assertInstanceOf(Chain::class, $chainRoute); + + $request = new ServerRequest([], [], new Uri('/foo/bar')); + $this->assertTrue($chainRoute->match($request)->isSuccess()); + } + + public function testCreateRouteWithChainedWithNoRouteName() + { + $spec = [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/', + ], + 'chain_routes' => [ + [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/', + ], + ], + [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/', + ], + ], + ], + ]; + + $chainRoute = $this->factory->routeFromSpec($spec); + $this->assertInstanceOf(Chain::class, $chainRoute); + + $chainedRoutes = array_keys($chainRoute->getRoutes()); + $this->assertCount(3, $chainedRoutes); + foreach ($chainedRoutes as $name) { + $this->assertStringMatchesFormat('__chained_route_no_name_%d', $name); + } + } + + public function testCreateRouteWithChildRoutes() + { + $spec = [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/foo', + ], + 'child_routes' => [ + 'child' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/bar', + ], + ], + ], + ]; + + $partRoute = $this->factory->routeFromSpec($spec); + $this->assertInstanceOf(Part::class, $partRoute); + + $request = new ServerRequest([], [], new Uri('/foo/bar')); + $this->assertTrue($partRoute->match($request)->isSuccess()); + } + + public function testCreateRouteWithChainedAndChildRoutes() + { + $spec = [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/foo', + ], + 'chain_routes' => [ + 'chained' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/bar', + ], + ], + ], + 'child_routes' => [ + 'child' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/baz', + ], + ], + ], + ]; + + $partRoute = $this->factory->routeFromSpec($spec); + $this->assertInstanceOf(Part::class, $partRoute); + + $request = new ServerRequest([], [], new Uri('/foo/bar/baz')); + $this->assertTrue($partRoute->match($request)->isSuccess()); + } + + public function testAddPrototype() + { + $route = new Literal('/'); + + $this->factory->addPrototype('test', $route); + $this->assertSame($route, $this->factory->getPrototype('test')); + } + + public function testGetNonExistentPrototype() + { + $this->assertNull($this->factory->getPrototype('test')); + } + + public function testCreateRouteFromPrototype() + { + $prototypeRoute = new Literal('/'); + $this->factory->addPrototype('test', $prototypeRoute); + + $route = $this->factory->routeFromSpec('test'); + $this->assertSame($prototypeRoute, $route); + } + + public function testCreateRouteFromNonExistantPrototypeShouldThrow() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Could not find prototype with name test'); + $this->factory->routeFromSpec('test'); + } +} From e0561e7fbc1e5db683a31744cdc7f3b23bfead22 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Tue, 13 Mar 2018 05:34:04 +1000 Subject: [PATCH 33/47] Move chain workaround to chain route factory --- src/Route/Chain.php | 19 ++++++++++++++++--- src/RouteConfigFactory.php | 9 --------- test/Route/ChainTest.php | 20 ++++++++++++++++++-- test/RouteConfigFactoryTest.php | 15 +++++---------- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/Route/Chain.php b/src/Route/Chain.php index dba9795..808634c 100644 --- a/src/Route/Chain.php +++ b/src/Route/Chain.php @@ -26,7 +26,9 @@ use function array_reverse; use function end; use function is_array; +use function is_numeric; use function key; +use function sprintf; /** * Chain route. @@ -35,6 +37,11 @@ class Chain extends SimpleRouteStack implements PartialRouteInterface { use PartialRouteTrait; + /** + * @var int + */ + static private $chainedIndex = 0; + /** * List of assembled parameters. * @@ -69,9 +76,15 @@ public static function factory(iterable $options = []) : self $options['routes'] = ArrayUtils::iteratorToArray($options['routes']); } - return new static( - $options['routes'] - ); + $routes = []; + foreach ($options['routes'] as $name => $route) { + if (is_numeric($name)) { + $name = sprintf('__chained_route_no_name_%d', self::$chainedIndex++); + } + $routes[$name] = $route; + } + + return new static($routes); } /** diff --git a/src/RouteConfigFactory.php b/src/RouteConfigFactory.php index 89f1ba1..497df1f 100644 --- a/src/RouteConfigFactory.php +++ b/src/RouteConfigFactory.php @@ -14,7 +14,6 @@ use function array_unshift; use function is_array; -use function is_numeric; use function is_string; use function sprintf; @@ -30,11 +29,6 @@ class RouteConfigFactory */ private $prototypes = []; - /** - * @var int - */ - static private $chainedIndex = 0; - public function __construct(RoutePluginManager $routes) { $this->routes = $routes; @@ -127,9 +121,6 @@ private function createChainFromSpec(array $specs) : RouteInterface $chainRoutes = []; foreach ($chainRoutesSpec as $name => $routeSpec) { - if (is_numeric($name)) { - $name = sprintf('__chained_route_no_name_%d', self::$chainedIndex++); - } $chainRoutes[$name] = $this->routeFromSpec($routeSpec); } diff --git a/test/Route/ChainTest.php b/test/Route/ChainTest.php index 9a052a1..8171812 100644 --- a/test/Route/ChainTest.php +++ b/test/Route/ChainTest.php @@ -19,9 +19,7 @@ use Zend\Router\Route\Method; use Zend\Router\Route\Segment; use Zend\Router\RouteInterface; -use Zend\Router\RoutePluginManager; use Zend\Router\RouteResult; -use Zend\ServiceManager\ServiceManager; use Zend\Stdlib\ArrayObject; use ZendTest\Router\FactoryTester; use ZendTest\Router\Route\TestAsset\RouteTestDefinition; @@ -254,4 +252,22 @@ public function testFactory() ] ); } + + public function testFactoryConvertsNumericKeysToString() + { + $chain = Chain::factory([ + 'routes' => [ + new Literal('/'), + new Literal('/'), + new Literal('/'), + ], + ]); + + $chained = $chain->getRoutes(); + $this->assertCount(3, $chained); + + foreach ($chained as $name => $route) { + $this->assertStringMatchesFormat('__chained_route_no_name_%d', $name); + } + } } diff --git a/test/RouteConfigFactoryTest.php b/test/RouteConfigFactoryTest.php index 1fb376e..d0ee271 100644 --- a/test/RouteConfigFactoryTest.php +++ b/test/RouteConfigFactoryTest.php @@ -24,8 +24,6 @@ use Zend\ServiceManager\Factory\FactoryInterface; use Zend\ServiceManager\ServiceManager; -use function array_keys; - /** * @covers \Zend\Router\RouteConfigFactory */ @@ -111,19 +109,19 @@ public function testCreateRouteWithChainedWithNoRouteName() $spec = [ 'type' => Literal::class, 'options' => [ - 'route' => '/', + 'route' => '/foo', ], 'chain_routes' => [ [ 'type' => Literal::class, 'options' => [ - 'route' => '/', + 'route' => '/bar', ], ], [ 'type' => Literal::class, 'options' => [ - 'route' => '/', + 'route' => '/baz', ], ], ], @@ -132,11 +130,8 @@ public function testCreateRouteWithChainedWithNoRouteName() $chainRoute = $this->factory->routeFromSpec($spec); $this->assertInstanceOf(Chain::class, $chainRoute); - $chainedRoutes = array_keys($chainRoute->getRoutes()); - $this->assertCount(3, $chainedRoutes); - foreach ($chainedRoutes as $name) { - $this->assertStringMatchesFormat('__chained_route_no_name_%d', $name); - } + $request = new ServerRequest([], [], new Uri('/foo/bar/baz')); + $this->assertTrue($chainRoute->match($request)->isSuccess()); } public function testCreateRouteWithChildRoutes() From 94b1a5db008562ec1e38c00485a371cf12174942 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 00:30:18 +1000 Subject: [PATCH 34/47] Add psr container to direct dependencies --- composer.json | 1 + composer.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 02f7e0a..7d5b4ae 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "require": { "php": "^7.1", "container-interop/container-interop": "^1.2", + "psr/container": "^1.0", "psr/http-message": "^1.0", "zendframework/zend-diactoros": "^1.7", "zendframework/zend-servicemanager": "^3.3", diff --git a/composer.lock b/composer.lock index fd22fa0..ca5e96c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "b20ef22f7463e54de613db8403c3daff", + "content-hash": "803b44bdf8a54be6b1ec76608d7655d0", "packages": [ { "name": "container-interop/container-interop", From 4ce472969fddbab558394b7fbb589569e152130e Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 00:48:55 +1000 Subject: [PATCH 35/47] Remove prototypes from factory and accept as extra parameter instead --- src/RouteConfigFactory.php | 48 +++++++++++++-------------------- test/RouteConfigFactoryTest.php | 48 +++++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/RouteConfigFactory.php b/src/RouteConfigFactory.php index 497df1f..fc0ab02 100644 --- a/src/RouteConfigFactory.php +++ b/src/RouteConfigFactory.php @@ -13,7 +13,10 @@ use Zend\Router\Exception\RuntimeException; use function array_unshift; +use function get_class; +use function gettype; use function is_array; +use function is_object; use function is_string; use function sprintf; @@ -24,11 +27,6 @@ class RouteConfigFactory */ private $routes; - /** - * @var array - */ - private $prototypes = []; - public function __construct(RoutePluginManager $routes) { $this->routes = $routes; @@ -41,16 +39,24 @@ public function __construct(RoutePluginManager $routes) * @throws RuntimeException * @throws InvalidArgumentException */ - public function routeFromSpec($spec) : RouteInterface + public function routeFromSpec($spec, array $prototypes = []) : RouteInterface { if ($spec instanceof RouteInterface) { return $spec; } if (is_string($spec)) { - if (null === ($route = $this->getPrototype($spec))) { + $route = $prototypes[$spec] ?? null; + if (null === $route) { throw new RuntimeException(sprintf('Could not find prototype with name %s', $spec)); } + if (! $route instanceof RouteInterface) { + throw new RuntimeException(sprintf( + 'Invalid prototype provided. Expected %s got %s', + RouteInterface::class, + is_object($route) ? get_class($route) : gettype($route) + )); + } return $route; } @@ -60,7 +66,7 @@ public function routeFromSpec($spec) : RouteInterface } if (isset($spec['chain_routes'])) { - $route = $this->createChainFromSpec($spec); + $route = $this->createChainFromSpec($spec, $prototypes); } else { if (! isset($spec['type'])) { throw new InvalidArgumentException('Missing "type" option'); @@ -78,34 +84,18 @@ public function routeFromSpec($spec) : RouteInterface } if (isset($spec['child_routes'])) { - $route = $this->createPartFromSpec($spec, $route); + $route = $this->createPartFromSpec($spec, $route, $prototypes); } return $route; } - /** - * Returns defined prototype route - */ - public function getPrototype(string $name) : ?RouteInterface - { - return $this->prototypes[$name] ?? null; - } - - /** - * Defines prototype route to be re-used when creating routes from spec - */ - public function addPrototype(string $name, RouteInterface $route) : void - { - $this->prototypes[$name] = $route; - } - /** * Wraps route in spec with Chain route, adds chain_routes to chain * * @throws InvalidArgumentException */ - private function createChainFromSpec(array $specs) : RouteInterface + private function createChainFromSpec(array $specs, array $prototypes) : RouteInterface { if (! is_array($specs['chain_routes'])) { throw new InvalidArgumentException('Chain routes must be an array'); @@ -121,7 +111,7 @@ private function createChainFromSpec(array $specs) : RouteInterface $chainRoutes = []; foreach ($chainRoutesSpec as $name => $routeSpec) { - $chainRoutes[$name] = $this->routeFromSpec($routeSpec); + $chainRoutes[$name] = $this->routeFromSpec($routeSpec, $prototypes); } $options = [ @@ -134,11 +124,11 @@ private function createChainFromSpec(array $specs) : RouteInterface /** * Wraps route in spec with Part route and adds child_routes to Part */ - private function createPartFromSpec(array $specs, RouteInterface $route) : RouteInterface + private function createPartFromSpec(array $specs, RouteInterface $route, array $prototypes) : RouteInterface { $childRoutes = []; foreach ($specs['child_routes'] as $name => $childSpec) { - $childRoutes[$name] = $this->routeFromSpec($childSpec); + $childRoutes[$name] = $this->routeFromSpec($childSpec, $prototypes); } $options = [ 'route' => $route, diff --git a/test/RouteConfigFactoryTest.php b/test/RouteConfigFactoryTest.php index d0ee271..b80c47f 100644 --- a/test/RouteConfigFactoryTest.php +++ b/test/RouteConfigFactoryTest.php @@ -24,6 +24,8 @@ use Zend\ServiceManager\Factory\FactoryInterface; use Zend\ServiceManager\ServiceManager; +use function sprintf; + /** * @covers \Zend\Router\RouteConfigFactory */ @@ -190,32 +192,50 @@ public function testCreateRouteWithChainedAndChildRoutes() $this->assertTrue($partRoute->match($request)->isSuccess()); } - public function testAddPrototype() + public function testCreateRouteFromPrototype() { - $route = new Literal('/'); - - $this->factory->addPrototype('test', $route); - $this->assertSame($route, $this->factory->getPrototype('test')); - } + $prototypeRoute = new Literal('/'); + $prototypes = ['test' => $prototypeRoute]; - public function testGetNonExistentPrototype() - { - $this->assertNull($this->factory->getPrototype('test')); + $route = $this->factory->routeFromSpec('test', $prototypes); + $this->assertSame($prototypeRoute, $route); } - public function testCreateRouteFromPrototype() + public function testCreateChildRouteFromPrototype() { $prototypeRoute = new Literal('/'); - $this->factory->addPrototype('test', $prototypeRoute); + $prototypes = ['test-prototype' => $prototypeRoute]; - $route = $this->factory->routeFromSpec('test'); - $this->assertSame($prototypeRoute, $route); + $spec = [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/', + ], + 'child_routes' => [ + 'test' => 'test-prototype', + ], + ]; + + $route = $this->factory->routeFromSpec($spec, $prototypes); + $this->assertInstanceOf(Part::class, $route); + $this->assertSame($prototypeRoute, $route->getRoute('test')); } public function testCreateRouteFromNonExistantPrototypeShouldThrow() { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not find prototype with name test'); - $this->factory->routeFromSpec('test'); + $this->factory->routeFromSpec('test', []); + } + + public function testCreateRouteFromInvalidPrototypeShouldThrow() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(sprintf( + 'Invalid prototype provided. Expected %s got %s', + RouteInterface::class, + 'string' + )); + $this->factory->routeFromSpec('test', ['test' => Literal::class]); } } From 1b52b4ba88e5bd409a4123265bb046fea7fce05c Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 01:25:33 +1000 Subject: [PATCH 36/47] Add Router class and its factory --- src/Container/RouterFactory.php | 63 ++++++++ src/Router.php | 127 ++++++++++++++++ test/Container/RouterFactoryTest.php | 109 ++++++++++++++ test/RouterTest.php | 207 +++++++++++++++++++++++++++ 4 files changed, 506 insertions(+) create mode 100644 src/Container/RouterFactory.php create mode 100644 src/Router.php create mode 100644 test/Container/RouterFactoryTest.php create mode 100644 test/RouterTest.php diff --git a/src/Container/RouterFactory.php b/src/Container/RouterFactory.php new file mode 100644 index 0000000..0e26a1c --- /dev/null +++ b/src/Container/RouterFactory.php @@ -0,0 +1,63 @@ +get(RouteConfigFactory::class), + $this->getRouteStack($container), + $container->get(UriInterface::class) + ); + $this->configureRouter($container, $router); + + return $router; + } + + public function getRouteStack(ContainerInterface $container) : RouteStackInterface + { + return new TreeRouteStack(); + } + + public function configureRouter(ContainerInterface $container, Router $router) : void + { + $config = $this->getRouterConfig($container); + + foreach ($config['prototypes'] as $name => $prototype) { + $router->addPrototype($name, $prototype); + } + + foreach ($config['routes'] as $name => $route) { + $router->addRoute($name, $route); + } + } + + public function getRouterConfig(ContainerInterface $container) : array + { + return [ + 'routes' => [], + 'prototypes' => [], + ]; + } +} diff --git a/src/Router.php b/src/Router.php new file mode 100644 index 0000000..d58a47a --- /dev/null +++ b/src/Router.php @@ -0,0 +1,127 @@ +routeFactory = $routeFactory; + $this->routeStack = $routeStack; + $this->uriFactory = $uriFactory; + } + + public function getRouteFactory() : RouteConfigFactory + { + return $this->routeFactory; + } + + public function setRouteStack(RouteStackInterface $routeStack) : void + { + $this->routeStack = $routeStack; + } + + public function getRouteStack() : RouteStackInterface + { + return $this->routeStack; + } + + /** + * Add route to the underlying route stack. + * + * @param array|string|RouteInterface $routeOrSpec Route instance, array + * specification or string name of prototype route to add + */ + public function addRoute(string $name, $routeOrSpec) : void + { + $this->routeStack->addRoute($name, $this->routeFactory->routeFromSpec($routeOrSpec, $this->prototypes)); + } + + /** + * Add reusable "prototype" route to be used when adding routers as + * array or string specification + * + * @param array|RouteInterface $routeOrSpec + */ + public function addPrototype(string $name, $routeOrSpec) : void + { + $this->prototypes[$name] = $this->routeFactory->routeFromSpec($routeOrSpec); + } + + /** + * Get reusable "prototype" route + */ + public function getPrototype(string $name) : ?RouteInterface + { + return $this->prototypes[$name] ?? null; + } + + /** + * Get registered "prototype" routes + */ + public function getPrototypes() : array + { + return $this->prototypes; + } + + /** + * Match request using configured route stack + */ + public function match(Request $request) : RouteResult + { + return $this->routeStack->match($request, 0); + } + + /** + * Assemble uri using configured route stack + */ + public function assemble(string $name, array $params, array $options = []) : UriInterface + { + $options['name'] = $name; + $uri = ($this->uriFactory)(); + return $this->routeStack->assemble($uri, $params, $options); + } +} diff --git a/test/Container/RouterFactoryTest.php b/test/Container/RouterFactoryTest.php new file mode 100644 index 0000000..0ddf412 --- /dev/null +++ b/test/Container/RouterFactoryTest.php @@ -0,0 +1,109 @@ +container = $this->prophesize(ContainerInterface::class); + $this->container->get(RouteConfigFactory::class) + ->willReturn($routeFactory); + $this->container->get(UriInterface::class) + ->willReturn($uriFactory); + } + + public function testGetRouteStackInstantiatesTreeRouteStack() + { + $factory = new RouterFactory(); + $routeStack = $factory->getRouteStack($this->container->reveal()); + $this->assertInstanceOf(TreeRouteStack::class, $routeStack); + } + + public function testConfigureRouterUsesConfigProvidedByGetRouterConfig() + { + $factory = new class() extends RouterFactory { + public function getRouterConfig(ContainerInterface $container) : array + { + return [ + 'routes' => [ + 'test-route' => new Literal('/'), + 'test-prototype' => 'prototype', + ], + 'prototypes' => [ + 'prototype' => new Method('GET'), + ], + ]; + } + }; + $router = $factory->__invoke($this->container->reveal()); + $this->assertInstanceOf( + Method::class, + $router->getPrototype('prototype') + ); + $this->assertInstanceOf( + Literal::class, + $router->getRouteStack()->getRoute('test-route') + ); + $this->assertInstanceOf( + Method::class, + $router->getRouteStack()->getRoute('test-prototype') + ); + } + + public function testCreatesRouterWithRouteStackReturnedByGetRouteStack() + { + $factory = new class() extends RouterFactory { + public function getRouteStack(ContainerInterface $container) : RouteStackInterface + { + return new SimpleRouteStack(); + } + }; + + $router = $factory->__invoke($this->container->reveal()); + $this->assertInstanceOf(SimpleRouteStack::class, $router->getRouteStack()); + } + + public function testGetRouterConfigReturnsDefaultEmptyConfig() + { + $this->container->get(Argument::any())->shouldNotBeCalled(); + $factory = new RouterFactory(); + $config = $factory->getRouterConfig($this->container->reveal()); + $this->assertEquals(['routes' => [], 'prototypes' => []], $config); + } +} diff --git a/test/RouterTest.php b/test/RouterTest.php new file mode 100644 index 0000000..00e0c1b --- /dev/null +++ b/test/RouterTest.php @@ -0,0 +1,207 @@ +routeFactory = new RouteConfigFactory(new RoutePluginManager(new ServiceManager())); + $this->routeStack = new TreeRouteStack(); + $this->router = new Router($this->routeFactory, $this->routeStack, $uriFactory); + } + + public function testGetRouteFactoryReturnsComposedFactory() + { + $factory = $this->router->getRouteFactory(); + $this->assertSame($this->routeFactory, $factory); + } + + public function testGetRouteStackReturnsComposedRouteStack() + { + $routeStack = $this->router->getRouteStack(); + $this->assertSame($this->routeStack, $routeStack); + } + + public function testSetRouteStackReplacesRouteStack() + { + $routeStack = new SimpleRouteStack(); + $this->router->setRouteStack($routeStack); + $this->assertSame($routeStack, $this->router->getRouteStack()); + } + + public function testAddRouteAddsToUnderlyingRouteStack() + { + $route = $this->prophesize(RouteInterface::class); + $this->router->addRoute('test', $route->reveal()); + $routeStack = $this->router->getRouteStack(); + $this->assertTrue($routeStack->hasRoute('test')); + $this->assertSame($route->reveal(), $routeStack->getRoute('test')); + } + + public function testAddRouteCreatesRouteFromConfig() + { + $spec = [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/', + ], + ]; + $this->router->addRoute('test', $spec); + $routeStack = $this->router->getRouteStack(); + $this->assertTrue($routeStack->hasRoute('test')); + $this->assertInstanceOf(Literal::class, $routeStack->getRoute('test')); + } + + public function testByDefaultNoPrototypesRegistered() + { + $prototypes = $this->router->getPrototypes(); + $this->assertEmpty($prototypes); + } + + public function testAddPrototypeWithRouteAddsPrototype() + { + $route = $this->prophesize(RouteInterface::class); + $this->router->addPrototype('test', $route->reveal()); + $this->assertSame($route->reveal(), $this->router->getPrototype('test')); + } + + public function testAddPrototypeAsSpecCreatesRouteAndAddsAsPrototype() + { + $spec = [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/', + ], + ]; + $this->router->addPrototype('test', $spec); + $route = $this->router->getPrototype('test'); + $this->assertInstanceOf(Literal::class, $route); + } + + public function testGetPrototypesReturnsRegisteredPrototypes() + { + $route = $this->prophesize(RouteInterface::class); + $this->router->addPrototype('test', $route->reveal()); + $prototypes = $this->router->getPrototypes(); + $this->assertEquals(['test' => $route->reveal()], $prototypes); + } + + public function testAddRouteAsSpecUsesRegisteredPrototypes() + { + $route = $this->prophesize(RouteInterface::class); + $this->router->addPrototype('testPrototype', $route->reveal()); + + $this->router->addRoute('test', 'testPrototype'); + $this->assertSame( + $route->reveal(), + $this->router->getRouteStack()->getRoute('test') + ); + } + + public function testAddRoutePassesRouteConfigToRouteFactory() + { + $spec = [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/', + ], + ]; + $routeFactory = $this->prophesize(RouteConfigFactory::class); + $routeFactory->routeFromSpec($spec, []) + ->shouldBecalled() + ->willReturn($this->prophesize(RouteInterface::class)->reveal()); + $uriFactory = function () { + return new Uri(); + }; + $router = new Router($routeFactory->reveal(), new TreeRouteStack(), $uriFactory); + $router->addRoute('test', $spec); + } + + public function testProxiesMatchToUnderlyingRouteStackAndReturnsItsResult() + { + $request = new ServerRequest(); + $expectedResult = RouteResult::fromRouteFailure(); + $routeStack = $this->prophesize(RouteStackInterface::class); + $routeStack->match($request, 0) + ->shouldBeCalled() + ->willReturn($expectedResult); + + $this->router->setRouteStack($routeStack->reveal()); + + $result = $this->router->match($request); + + $this->assertSame($expectedResult, $result); + } + + public function testProxiesAssembleToUnderlyingRouteStackAndReturnsItsResult() + { + $uri = new Uri(); + $route = $this->prophesize(RouteInterface::class); + $route->assemble(Argument::any(), ['foo' => 'bar'], ['baz' => 'qux']) + ->willReturn($uri) + ->shouldBeCalled(); + + $this->router->addRoute('test', $route->reveal()); + + $returnedUri = $this->router->assemble('test', ['foo' => 'bar'], ['baz' => 'qux']); + $this->assertSame($uri, $returnedUri); + } + + public function testAssembleUsesUriClosureFactoryToCreateUriAndPassToRouteStackAssemble() + { + $uri = new Uri(); + $routeStack = $this->prophesize(RouteStackInterface::class); + $routeStack->assemble($uri, [], ['name' => 'test']) + ->shouldBeCalled(); + + $uriFactory = function () use ($uri) { + return $uri; + }; + $router = new Router($this->routeFactory, $routeStack->reveal(), $uriFactory); + $router->assemble('test', [], []); + } +} From d2634f95932e7027decf541aa80185e5b235e6cb Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 02:18:13 +1000 Subject: [PATCH 37/47] Reintroduce TranslatorAwareRouteStackDecorator inplace of dropped tree route stack --- src/TranslatorAwareRouteStackDecorator.php | 196 +++++++++++++ ...TranslatorAwareRouteStackDecoratorTest.php | 257 ++++++++++++++++++ 2 files changed, 453 insertions(+) create mode 100644 src/TranslatorAwareRouteStackDecorator.php create mode 100644 test/TranslatorAwareRouteStackDecoratorTest.php diff --git a/src/TranslatorAwareRouteStackDecorator.php b/src/TranslatorAwareRouteStackDecorator.php new file mode 100644 index 0000000..d50994d --- /dev/null +++ b/src/TranslatorAwareRouteStackDecorator.php @@ -0,0 +1,196 @@ +decoratedRouteStack = $decoratedRouteStack; + $this->setTranslator($translator); + } + + public function getDecoratedRouteStack() : RouteStackInterface + { + return $this->decoratedRouteStack; + } + + /** + * @param null|string $textDomain + */ + public function setTranslator(Translator $translator = null, $textDomain = null) : TranslatorAwareInterface + { + $this->translator = $translator; + + if ($textDomain !== null) { + $this->setTranslatorTextDomain($textDomain); + } + + return $this; + } + + public function getTranslator() : ?TranslatorInterface + { + return $this->translator; + } + + public function hasTranslator() : bool + { + return $this->translator !== null; + } + + /** + * @param bool $enabled + */ + public function setTranslatorEnabled($enabled = true) : TranslatorAwareInterface + { + $this->translatorEnabled = $enabled; + return $this; + } + + public function isTranslatorEnabled() : bool + { + return $this->translatorEnabled; + } + + /** + * @param string $textDomain + */ + public function setTranslatorTextDomain($textDomain = 'default') : TranslatorAwareInterface + { + $this->translatorTextDomain = $textDomain; + + return $this; + } + + public function getTranslatorTextDomain() : string + { + return $this->translatorTextDomain; + } + + /** + * Match a given request. + */ + public function match(Request $request, int $pathOffset = 0, array $options = []) : RouteResult + { + // translator always present + if ($this->isTranslatorEnabled() && ! isset($options['translator'])) { + $options['translator'] = $this->getTranslator(); + } + + if ($this->isTranslatorEnabled() && ! isset($options['text_domain'])) { + $options['text_domain'] = $this->getTranslatorTextDomain(); + } + return $this->decoratedRouteStack->match($request, $pathOffset, $options); + } + + /** + * Assemble the route. + */ + public function assemble(UriInterface $uri, array $params = [], array $options = []) : UriInterface + { + if ($this->isTranslatorEnabled() && ! isset($options['translator'])) { + $options['translator'] = $this->getTranslator(); + } + + if ($this->isTranslatorEnabled() && ! isset($options['text_domain'])) { + $options['text_domain'] = $this->getTranslatorTextDomain(); + } + return $this->decoratedRouteStack->assemble($uri, $params, $options); + } + + /** + * Add a route to the stack. + */ + public function addRoute(string $name, RouteInterface $route, int $priority = null) : void + { + $this->decoratedRouteStack->addRoute($name, $route, $priority); + } + + /** + * Add multiple routes to the stack. + */ + public function addRoutes(iterable $routes) : void + { + $this->decoratedRouteStack->addRoutes($routes); + } + + /** + * Remove a route from the stack. + */ + public function removeRoute(string $name) : void + { + $this->decoratedRouteStack->removeRoute($name); + } + + /** + * Remove all routes from the stack and set new ones. + */ + public function setRoutes(iterable $routes) : void + { + $this->decoratedRouteStack->setRoutes($routes); + } + + /** + * Get the added routes + */ + public function getRoutes() : array + { + return $this->decoratedRouteStack->getRoutes(); + } + + /** + * Check if a route with a specific name exists + */ + public function hasRoute(string $name) : bool + { + return $this->decoratedRouteStack->hasRoute($name); + } + + /** + * Get a route by name + */ + public function getRoute(string $name) : ?RouteInterface + { + return $this->decoratedRouteStack->getRoute($name); + } +} diff --git a/test/TranslatorAwareRouteStackDecoratorTest.php b/test/TranslatorAwareRouteStackDecoratorTest.php new file mode 100644 index 0000000..8da53da --- /dev/null +++ b/test/TranslatorAwareRouteStackDecoratorTest.php @@ -0,0 +1,257 @@ +translator = $this->prophesize(TranslatorInterface::class); + $this->routeStack = $this->prophesize(RouteStackInterface::class); + $this->decorator = new TranslatorAwareRouteStackDecorator( + $this->routeStack->reveal(), + $this->translator->reveal() + ); + } + + public function testGetDecoratedRouteStack() + { + $this->assertSame($this->routeStack->reveal(), $this->decorator->getDecoratedRouteStack()); + } + + public function testGetTranslator() + { + $this->assertSame($this->translator->reveal(), $this->decorator->getTranslator()); + } + + public function testSetTranslator() + { + $translator = $this->prophesize(TranslatorInterface::class) + ->reveal(); + $this->decorator->setTranslator($translator); + $this->assertSame($translator, $this->decorator->getTranslator()); + } + + public function testHasTranslator() + { + // translator is constructor injected and always present + $this->assertTrue($this->decorator->hasTranslator()); + } + + public function testSetTranslatorEnabled() + { + $this->assertTrue($this->decorator->isTranslatorEnabled()); + $this->decorator->setTranslatorEnabled(false); + $this->assertFalse($this->decorator->isTranslatorEnabled()); + } + + public function testTranslatorEnabledByDefault() + { + $this->assertTrue($this->decorator->isTranslatorEnabled()); + } + + public function testSetTranslatorTextDomain() + { + $this->decorator->setTranslatorTextDomain('foo'); + $this->assertEquals('foo', $this->decorator->getTranslatorTextDomain()); + + $this->decorator->setTranslator($this->translator->reveal(), 'bar'); + $this->assertEquals('bar', $this->decorator->getTranslatorTextDomain()); + } + + public function testGetTranslatorTextDomain() + { + $this->assertEquals('default', $this->decorator->getTranslatorTextDomain()); + } + + public function testMatchProxiesToDecoratedRouteStack() + { + $request = new ServerRequest(); + $options = ['foo' => 'bar']; + $result = RouteResult::fromRouteFailure(); + $this->routeStack->match($request, 1, $options) + ->willReturn($result) + ->shouldBeCalled(); + + $this->decorator->setTranslatorEnabled(false); + $returnedResult = $this->decorator->match($request, 1, $options); + $this->assertSame($result, $returnedResult); + } + + public function testMatchAddsTextDomainAndTranslatorWhenTranslatorEnabled() + { + $request = new ServerRequest(); + $options = ['foo' => 'bar']; + $result = RouteResult::fromRouteFailure(); + $expectedOptions = $options; + $expectedOptions['text_domain'] = 'default'; + $expectedOptions['translator'] = $this->translator->reveal(); + $this->routeStack->match($request, 1, $expectedOptions) + ->willReturn($result) + ->shouldBeCalled(); + + $this->decorator->setTranslatorEnabled(true); + $returnedResult = $this->decorator->match($request, 1, $options); + $this->assertSame($result, $returnedResult); + } + + /** + * Matches v3 TranslatorAwareTreeRouteStack behavior. From design + * standpoint it should at least hard fail if 'translator' option is not + * instance of TranslatorInterface to avoid hard to debug unexpected behavior. + */ + public function testMatchDoesNotOverrideTranslatorOrTextDomainOptions() + { + $request = new ServerRequest(); + $options = ['foo' => 'bar', 'text_domain' => 'another', 'translator' => 'foo']; + $result = RouteResult::fromRouteFailure(); + $this->routeStack->match($request, 1, $options) + ->willReturn($result) + ->shouldBeCalled(); + + $this->decorator->setTranslatorEnabled(true); + $returnedResult = $this->decorator->match($request, 1, $options); + $this->assertSame($result, $returnedResult); + } + + public function testAssembleProxiesToDecoratedRouteStack() + { + $uri = new Uri(); + $expectUri = new Uri(); + $options = ['foo' => 'bar']; + $params = ['baz' => 'qux']; + $this->routeStack->assemble($uri, $params, $options) + ->willReturn($expectUri) + ->shouldBeCalled(); + + $this->decorator->setTranslatorEnabled(false); + $returnedUri = $this->decorator->assemble($uri, $params, $options); + $this->assertSame($expectUri, $returnedUri); + } + + public function testAssembleAddsTextDomainAndTranslatorWhenTranslatorEnabled() + { + $uri = new Uri(); + $expectUri = new Uri(); + $options = ['foo' => 'bar']; + $params = ['baz' => 'qux']; + $expectedOptions = $options; + $expectedOptions['text_domain'] = 'default'; + $expectedOptions['translator'] = $this->translator->reveal(); + $this->routeStack->assemble($uri, $params, $expectedOptions) + ->willReturn($expectUri) + ->shouldBeCalled(); + + $this->decorator->setTranslatorEnabled(true); + $returnedUri = $this->decorator->assemble($uri, $params, $options); + $this->assertSame($expectUri, $returnedUri); + } + + public function testAssembleDoesNotOverrideTextDomainAndTranslatorOptions() + { + $uri = new Uri(); + $expectUri = new Uri(); + $options = ['foo' => 'bar', 'text_domain' => 'another', 'translator' => 'foo']; + $params = ['baz' => 'qux']; + $this->routeStack->assemble($uri, $params, $options) + ->willReturn($expectUri) + ->shouldBeCalled(); + + $this->decorator->setTranslatorEnabled(true); + $returnedUri = $this->decorator->assemble($uri, $params, $options); + $this->assertSame($expectUri, $returnedUri); + } + + public function testAddRoute() + { + $route = $this->prophesize(RouteInterface::class)->reveal(); + $this->routeStack->addRoute('test', $route, 10) + ->shouldBeCalled(); + $this->decorator->addRoute('test', $route, 10); + } + + public function testAddRoutes() + { + $route = $this->prophesize(RouteInterface::class)->reveal(); + $this->routeStack->addRoutes(['test' => $route]) + ->shouldBeCalled(); + $this->decorator->addRoutes(['test' => $route]); + } + + public function testRemoveRoute() + { + $this->routeStack->removeRoute('test') + ->shouldBeCalled(); + $this->decorator->removeRoute('test'); + } + + public function testSetRoutes() + { + $route = $this->prophesize(RouteInterface::class)->reveal(); + $this->routeStack->setRoutes(['test' => $route]) + ->shouldBeCalled(); + $this->decorator->setRoutes(['test' => $route]); + } + + public function testGetRoutes() + { + $route = $this->prophesize(RouteInterface::class)->reveal(); + $this->routeStack->getRoutes() + ->willReturn(['test' => $route]) + ->shouldBeCalled(); + $routes = $this->decorator->getRoutes(); + $this->assertEquals(['test' => $route], $routes); + } + + public function testHasRoute() + { + $this->routeStack->hasRoute('test') + ->shouldBeCalled(); + $this->decorator->hasRoute('test'); + } + + public function testGetRoute() + { + $route = $this->prophesize(RouteInterface::class)->reveal(); + $this->routeStack->getRoute('test') + ->willReturn($route) + ->shouldBeCalled(); + $returned = $this->decorator->getRoute('test'); + $this->assertSame($route, $returned); + } +} From 8196c4ef0a1befb3b6d955f2073c95255e51f851 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 02:22:52 +1000 Subject: [PATCH 38/47] Drop unused RouterFactory --- src/ConfigProvider.php | 7 +--- src/RouterFactory.php | 48 ------------------------ test/RouterFactoryTest.php | 75 -------------------------------------- 3 files changed, 1 insertion(+), 129 deletions(-) delete mode 100644 src/RouterFactory.php delete mode 100644 test/RouterFactoryTest.php diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 6a0a981..39b35ba 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -30,7 +30,7 @@ public function __invoke() { return [ 'dependencies' => $this->getDependencyConfig(), - 'route_manager' => $this->getRouteManagerConfig(), + RoutePluginManager::class => $this->getRouteManagerConfig(), ]; } @@ -43,15 +43,10 @@ public function getDependencyConfig() { return [ 'aliases' => [ - 'HttpRouter' => TreeRouteStack::class, - 'router' => RouteStackInterface::class, - 'Router' => RouteStackInterface::class, 'RoutePluginManager' => RoutePluginManager::class, ], 'factories' => [ - TreeRouteStack::class => Route\HttpRouterFactory::class, RoutePluginManager::class => RoutePluginManagerFactory::class, - RouteStackInterface::class => RouterFactory::class, ], ]; } diff --git a/src/RouterFactory.php b/src/RouterFactory.php deleted file mode 100644 index 6179f00..0000000 --- a/src/RouterFactory.php +++ /dev/null @@ -1,48 +0,0 @@ -get('HttpRouter'); - } - - /** - * Create and return RouteStackInterface instance - * - * For use with zend-servicemanager v2; proxies to __invoke(). - * - * @param ServiceLocatorInterface $container - * @param null|string $normalizedName - * @param null|string $requestedName - * @return RouteStackInterface - */ - public function createService(ServiceLocatorInterface $container, $normalizedName = null, $requestedName = null) - { - $requestedName = $requestedName ?: 'Router'; - return $this($container, $requestedName); - } -} diff --git a/test/RouterFactoryTest.php b/test/RouterFactoryTest.php deleted file mode 100644 index 03aca8b..0000000 --- a/test/RouterFactoryTest.php +++ /dev/null @@ -1,75 +0,0 @@ -defaultServiceConfig = [ - 'factories' => [ - 'HttpRouter' => HttpRouterFactory::class, - 'RoutePluginManager' => function ($services) { - return new RoutePluginManager($services); - }, - ], - ]; - - $this->factory = new RouterFactory(); - } - - private function createContainer() - { - return $this->prophesize(ContainerInterface::class)->reveal(); - } - - public function testFactoryCanCreateRouterBasedOnConfiguredName() - { - $config = new Config(array_merge_recursive($this->defaultServiceConfig, [ - 'services' => [ 'config' => [ - 'router' => [ - 'router_class' => TestAsset\Router::class, - ], - ]], - ])); - $services = new ServiceManager(); - $config->configureServiceManager($services); - - $router = $this->factory->__invoke($services, 'router'); - $this->assertInstanceOf(TestAsset\Router::class, $router); - } - - public function testFactoryCanCreateRouterWhenOnlyHttpRouterConfigPresent() - { - $config = new Config(array_merge_recursive($this->defaultServiceConfig, [ - 'services' => [ 'config' => [ - 'router' => [ - 'router_class' => TestAsset\Router::class, - ], - ]], - ])); - $services = new ServiceManager(); - $config->configureServiceManager($services); - - $router = $this->factory->__invoke($services, 'router'); - $this->assertInstanceOf(TestAsset\Router::class, $router); - } -} From af430fd4f080ad481a5b37826b9e5746b17e1b94 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 02:34:49 +1000 Subject: [PATCH 39/47] Improve tests for route plugin manager factory --- src/Container/RoutePluginManagerFactory.php | 3 ++ .../RoutePluginManagerFactoryTest.php | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/Container/RoutePluginManagerFactory.php b/src/Container/RoutePluginManagerFactory.php index 2fed528..a34dfd7 100644 --- a/src/Container/RoutePluginManagerFactory.php +++ b/src/Container/RoutePluginManagerFactory.php @@ -28,6 +28,9 @@ public function __invoke(ContainerInterface $container, $name, array $options = public function getRoutesConfig(ContainerInterface $container) : array { + if (! $container->has('config')) { + return []; + } return $container->get('config')[RoutePluginManager::class] ?? []; } } diff --git a/test/Container/RoutePluginManagerFactoryTest.php b/test/Container/RoutePluginManagerFactoryTest.php index 3826206..592b0fc 100644 --- a/test/Container/RoutePluginManagerFactoryTest.php +++ b/test/Container/RoutePluginManagerFactoryTest.php @@ -11,7 +11,9 @@ use Interop\Container\ContainerInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Prophecy\ObjectProphecy; use Zend\Router\Container\RoutePluginManagerFactory; +use Zend\Router\Route\Literal; use Zend\Router\RouteInterface; use Zend\Router\RoutePluginManager; @@ -20,6 +22,16 @@ */ class RoutePluginManagerFactoryTest extends TestCase { + /** + * @var ContainerInterface|ObjectProphecy + */ + private $container; + + /** + * @var RoutePluginManagerFactory + */ + private $factory; + public function setUp() { $this->container = $this->prophesize(ContainerInterface::class); @@ -32,6 +44,24 @@ public function testInvocationReturnsAPluginManager() $this->assertInstanceOf(RoutePluginManager::class, $plugins); } + public function testUsesRouteManagerConfigFromContainerWhenProvided() + { + $route = new Literal('/'); + $factory = function () use ($route) { + return $route; + }; + $this->container->has('config')->willReturn(true); + $this->container->get('config')->willReturn([ + RoutePluginManager::class => [ + 'factories' => [ + 'test' => $factory, + ], + ], + ]); + $routes = $this->factory->__invoke($this->container->reveal(), RoutePluginManager::class); + $this->assertSame($route, $routes->get('test')); + } + public function testInvocationCanProvideOptionsToThePluginManager() { $options = [ From f2aa83ccaf1fe8208175e537c012568ec4e4e3a2 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 02:39:48 +1000 Subject: [PATCH 40/47] Increment assertion count for test not doing assertions by intention --- test/PriorityListTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/test/PriorityListTest.php b/test/PriorityListTest.php index a7b5d09..a418d8e 100644 --- a/test/PriorityListTest.php +++ b/test/PriorityListTest.php @@ -53,6 +53,7 @@ public function testRemove() public function testRemovingNonExistentRouteDoesNotYieldError() { $this->list->remove('foo'); + $this->addToAssertionCount(1); } public function testClear() From 2c2bad557fc257e7b1fe655eeedd923f6dc83412 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 02:57:07 +1000 Subject: [PATCH 41/47] Remove service manager v2 support in RouteInvokableFactory --- src/RouteInvokableFactory.php | 105 +++++++++------------------------- test/TestAsset/DummyRoute.php | 5 ++ 2 files changed, 32 insertions(+), 78 deletions(-) diff --git a/src/RouteInvokableFactory.php b/src/RouteInvokableFactory.php index bab50a5..32b17a2 100644 --- a/src/RouteInvokableFactory.php +++ b/src/RouteInvokableFactory.php @@ -11,9 +11,12 @@ use Interop\Container\ContainerInterface; use Zend\ServiceManager\Exception\ServiceNotCreatedException; -use Zend\ServiceManager\AbstractFactoryInterface; -use Zend\ServiceManager\FactoryInterface; -use Zend\ServiceManager\ServiceLocatorInterface; +use Zend\ServiceManager\Factory\AbstractFactoryInterface; + +use function class_exists; +use function is_subclass_of; +use function method_exists; +use function sprintf; /** * Specialized invokable/abstract factory for use with RoutePluginManager. @@ -21,27 +24,17 @@ * 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, - FactoryInterface +class RouteInvokableFactory implements AbstractFactoryInterface { /** - * Options used to create instance (used with zend-servicemanager v2) - * - * @var array - */ - protected $creationOptions = []; - - /** - * 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. + * Only works for FQCN $routeName values, for classes that implement RouteInterface + * and have factory method. * - * @param ContainerInterface $container * @param string $routeName - * @return bool */ - public function canCreate(ContainerInterface $container, $routeName) + public function canCreate(ContainerInterface $container, $routeName) : bool { if (! class_exists($routeName)) { return false; @@ -51,39 +44,26 @@ public function canCreate(ContainerInterface $container, $routeName) return false; } - return true; - } + if (! method_exists($routeName, 'factory')) { + return false; + } - /** - * Can we create a route instance with the given name? (v2) - * - * Proxies to canCreate(). - * - * @param ServiceLocatorInterface $container - * @param string $normalizedName - * @param string $routeName - * @return bool - */ - public function canCreateServiceWithName(ServiceLocatorInterface $container, $normalizedName, $routeName) - { - return $this->canCreate($container, $routeName); + return true; } /** * Create and return a RouteInterface instance. * - * If the specified $routeName class does not exist or does not implement - * RouteInterface, this method will raise an exception. + * If the specified $routeName class does not exist, does not implement + * RouteInterface or does not provide factory method, this method will raise an exception. * * Otherwise, it uses the class' `factory()` method with the provided * $options to produce an instance. * - * @param ContainerInterface $container * @param string $routeName - * @param null|array $options - * @return RouteInterface + * @throws ServiceNotCreatedException */ - public function __invoke(ContainerInterface $container, $routeName, array $options = null) + public function __invoke(ContainerInterface $container, $routeName, array $options = null) : RouteInterface { $options = $options ?: []; @@ -104,45 +84,14 @@ public function __invoke(ContainerInterface $container, $routeName, array $optio )); } - return $routeName::factory($options); - } - - /** - * Create a route instance with the given name. (v2) - * - * Proxies to __invoke(). - * - * @param ServiceLocatorInterface $container - * @param string $normalizedName - * @param string $routeName - * @return RouteInterface - */ - public function createServiceWithName(ServiceLocatorInterface $container, $normalizedName, $routeName) - { - return $this($container, $routeName, $this->creationOptions); - } - - /** - * Create and return RouteInterface instance - * - * For use with zend-servicemanager v2; proxies to __invoke(). - * - * @param ServiceLocatorInterface $container - * @return RouteInterface - */ - public function createService(ServiceLocatorInterface $container, $normalizedName = null, $routeName = null) - { - $routeName = $routeName ?: RouteInterface::class; - return $this($container, $routeName, $this->creationOptions); - } + if (! method_exists($routeName, 'factory')) { + throw new ServiceNotCreatedException(sprintf( + '%s: failed retrieving invokable class "%s"; class does not provide factory method', + __CLASS__, + $routeName + )); + } - /** - * Set options to use when creating a service (v2) - * - * @param array $creationOptions - */ - public function setCreationOptions(array $creationOptions) - { - $this->creationOptions = $creationOptions; + return $routeName::factory($options); } } diff --git a/test/TestAsset/DummyRoute.php b/test/TestAsset/DummyRoute.php index d53911d..ceb3dd1 100644 --- a/test/TestAsset/DummyRoute.php +++ b/test/TestAsset/DummyRoute.php @@ -19,6 +19,11 @@ */ class DummyRoute implements RouteInterface { + public static function factory(array $options) : self + { + return new static(); + } + public function match(Request $request, int $pathOffset = 0, array $options = []) : RouteResult { return RouteResult::fromRouteMatch([]); From d11ff0fa2f9d9afd6f06c7991da7d80eaf4f0315 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 03:02:22 +1000 Subject: [PATCH 42/47] Remove unused test assets --- test/Route/TestAsset/DummyRoute.php | 68 --------------- test/Route/TestAsset/DummyRouteWithParam.php | 49 ----------- test/TestAsset/Router.php | 90 -------------------- 3 files changed, 207 deletions(-) delete mode 100644 test/Route/TestAsset/DummyRoute.php delete mode 100644 test/Route/TestAsset/DummyRouteWithParam.php delete mode 100644 test/TestAsset/Router.php diff --git a/test/Route/TestAsset/DummyRoute.php b/test/Route/TestAsset/DummyRoute.php deleted file mode 100644 index 706dd37..0000000 --- a/test/Route/TestAsset/DummyRoute.php +++ /dev/null @@ -1,68 +0,0 @@ - $pathOffset], -4); - } - - /** - * assemble(): defined by RouteInterface interface. - * - * @see Route::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = null, array $options = null) - { - return ''; - } - - /** - * factory(): defined by RouteInterface interface - * - * @param array|Traversable $options - * @return DummyRoute - */ - public static function factory($options = []) - { - return new static(); - } - - /** - * getAssembledParams(): defined by RouteInterface interface. - * - * @see Route::getAssembledParams - * @return array - */ - public function getAssembledParams() - { - return []; - } -} diff --git a/test/Route/TestAsset/DummyRouteWithParam.php b/test/Route/TestAsset/DummyRouteWithParam.php deleted file mode 100644 index 346343e..0000000 --- a/test/Route/TestAsset/DummyRouteWithParam.php +++ /dev/null @@ -1,49 +0,0 @@ - 'bar'], -4); - } - - /** - * assemble(): defined by RouteInterface interface. - * - * @see Route::assemble() - * @param array $params - * @param array $options - * @return mixed - */ - public function assemble(array $params = null, array $options = null) - { - if (isset($params['foo'])) { - return $params['foo']; - } - - return ''; - } -} diff --git a/test/TestAsset/Router.php b/test/TestAsset/Router.php deleted file mode 100644 index a8c5e3b..0000000 --- a/test/TestAsset/Router.php +++ /dev/null @@ -1,90 +0,0 @@ - Date: Thu, 15 Mar 2018 03:19:50 +1000 Subject: [PATCH 43/47] Comment out and keep test for ambiguous use case having less desired behavior --- test/Route/HostnameTest.php | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/test/Route/HostnameTest.php b/test/Route/HostnameTest.php index 0351b7d..2eada67 100644 --- a/test/Route/HostnameTest.php +++ b/test/Route/HostnameTest.php @@ -193,26 +193,28 @@ public function getRouteTestDefinitions() : iterable ->shouldAssembleAndExpectResultSameAsUriForMatching() ->useParamsForAssemble(['foo' => null]); - /** - * @todo investigate if this can be fixed or should be documented as a quirk + /* + * This case is left here as a reference of ambiguous use case. It should + * probably be fixed to provide more sensible behavior. * - * There is a workaround, [[:foo.]:bar.] removes ambiguity. Fix could + * There is a workaround, [[:foo.]:bar.] removes ambiguity. Fix could * be by emulating such nesting for same level optional parts, but it * might break other use cases + * + * $params = ['bar' => 'bat']; + * yield 'optional parameters evaluated right to left' => (new RouteTestDefinition( + * new Hostname('[:foo.][:bar.]example.com'), + * (new Uri())->withHost('bat.example.com') + * )) + * ->expectMatchResult( + * RouteResult::fromRouteMatch($params) + * ) + * ->expectPartialMatchResult( + * PartialRouteResult::fromRouteMatch($params, 0, 0) + * ) + * ->shouldAssembleAndExpectResultSameAsUriForMatching() + * ->useParamsForAssemble($params); */ - $params = ['bar' => 'bat']; - yield 'optional parameters evaluated right to left' => (new RouteTestDefinition( - new Hostname('[:foo.][:bar.]example.com'), - (new Uri())->withHost('bat.example.com') - )) - ->expectMatchResult( - RouteResult::fromRouteMatch($params) - ) - ->expectPartialMatchResult( - PartialRouteResult::fromRouteMatch($params, 0, 0) - ) - ->shouldAssembleAndExpectResultSameAsUriForMatching() - ->useParamsForAssemble($params); yield 'two missing optional subdomain' => (new RouteTestDefinition( new Hostname('[:foo.][:bar.]example.com'), From c342542eb22b090c6af343181ef41f7e007aa983 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 03:28:54 +1000 Subject: [PATCH 44/47] Remove obsolete RouterConfigTrait --- src/RouterConfigTrait.php | 40 --------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 src/RouterConfigTrait.php diff --git a/src/RouterConfigTrait.php b/src/RouterConfigTrait.php deleted file mode 100644 index 0e20e9c..0000000 --- a/src/RouterConfigTrait.php +++ /dev/null @@ -1,40 +0,0 @@ -get('RoutePluginManager'); - $config['route_plugins'] = $routePluginManager; - } - - // Obtain an instance - $factory = sprintf('%s::factory', $class); - return call_user_func($factory, $config); - } -} From 14a1b514a92b8972dd53fc6823e6a059dbcb1c72 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 03:30:01 +1000 Subject: [PATCH 45/47] Apply cs fixes --- src/Exception/DomainException.php | 1 + src/PartialRouteInterface.php | 1 + src/RouteInterface.php | 2 +- src/RouteMatch.php | 5 ++++- src/RouteResult.php | 6 ++++++ test/FactoryTester.php | 7 +------ test/Route/TestAsset/RouteTestDefinition.php | 22 +++++++++++++++----- 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/Exception/DomainException.php b/src/Exception/DomainException.php index a7d1ae2..83c696d 100644 --- a/src/Exception/DomainException.php +++ b/src/Exception/DomainException.php @@ -4,6 +4,7 @@ * @copyright Copyright (c) 2005-2017 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ + declare(strict_types=1); namespace Zend\Router\Exception; diff --git a/src/PartialRouteInterface.php b/src/PartialRouteInterface.php index b50c7b8..a9c33e1 100644 --- a/src/PartialRouteInterface.php +++ b/src/PartialRouteInterface.php @@ -4,6 +4,7 @@ * @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (http://www.zend.com) * @license https://github.com/zendframework/zend-router/blob/master/LICENSE.md New BSD License */ + declare(strict_types=1); namespace Zend\Router; diff --git a/src/RouteInterface.php b/src/RouteInterface.php index a228972..8bb639c 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -9,8 +9,8 @@ namespace Zend\Router; -use Psr\Http\Message\UriInterface; use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\UriInterface; /** * RouteInterface interface. diff --git a/src/RouteMatch.php b/src/RouteMatch.php index b2693c7..da722a7 100644 --- a/src/RouteMatch.php +++ b/src/RouteMatch.php @@ -42,6 +42,9 @@ public function __construct(array $params) $this->params = $params; } + /** + * @throws RuntimeException + */ public static function fromRouteResult(RouteResult $result) : self { if (! $result->isSuccess()) { @@ -102,7 +105,7 @@ public function getParams() * Get a specific parameter. * * @param string $name - * @param null|mixed $default + * @param mixed $default * @return mixed */ public function getParam($name, $default = null) diff --git a/src/RouteResult.php b/src/RouteResult.php index c85f2cf..7c0641f 100644 --- a/src/RouteResult.php +++ b/src/RouteResult.php @@ -74,6 +74,8 @@ public static function fromRouteFailure() : self /** * Create routing failure result where http method is not allowed for the * otherwise routable request + * + * @throws DomainException */ public static function fromMethodFailure(array $allowedMethods) : self { @@ -121,6 +123,8 @@ public function isMethodFailure() : bool * - {@see RouteResult::NAME_REPLACE} replaces existing route name * - {@see RouteResult::NAME_PREPEND} prepends as a parent route part name. * - {@see RouteResult::NAME_APPEND} appends as a child route part name. + * @throws DomainException + * @throws RuntimeException */ public function withMatchedRouteName(string $routeName, $flag = self::NAME_REPLACE) : self { @@ -153,6 +157,8 @@ public function withMatchedRouteName(string $routeName, $flag = self::NAME_REPLA /** * Produce a new route result with provided matched parameters. Can only be * used with successful result. + * + * @throws RuntimeException */ public function withMatchedParams(array $params) : self { diff --git a/test/FactoryTester.php b/test/FactoryTester.php index dc2630b..dd98f96 100644 --- a/test/FactoryTester.php +++ b/test/FactoryTester.php @@ -26,8 +26,6 @@ class FactoryTester /** * Create a new factory tester. - * - * @param TestCase $testCase */ public function __construct(TestCase $testCase) { @@ -36,11 +34,8 @@ 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 { // Test required options. foreach ($requiredOptions as $option => $exceptionMessage) { diff --git a/test/Route/TestAsset/RouteTestDefinition.php b/test/Route/TestAsset/RouteTestDefinition.php index cff6366..0d0010a 100644 --- a/test/Route/TestAsset/RouteTestDefinition.php +++ b/test/Route/TestAsset/RouteTestDefinition.php @@ -35,12 +35,12 @@ final class RouteTestDefinition private $matchRequest; /** - * @var ?RouteResult + * @var null|RouteResult */ private $matchResult; /** - * @var ?PartialRouteResult + * @var null|PartialRouteResult */ private $partialMatchResult; @@ -55,12 +55,12 @@ final class RouteTestDefinition private $matchOptions = []; /** - * @var ?UriInterface + * @var null|UriInterface */ private $assembleWithUri; /** - * @var ?UriInterface + * @var null|UriInterface */ private $assembleResult; @@ -74,6 +74,10 @@ final class RouteTestDefinition */ private $assembleOptions = []; + /** + * @param ServerRequestInterface|UriInterface $requestOrUriToMatch + * @throws Exception + */ public function __construct(RouteInterface $route, $requestOrUriToMatch) { $this->route = $route; @@ -102,6 +106,9 @@ public function expectMatchResult(RouteResult $result) : self return $this; } + /** + * @throws Exception + */ public function getExpectedMatchResult() : RouteResult { if (! $this->matchResult) { @@ -112,7 +119,9 @@ public function getExpectedMatchResult() : RouteResult return $this->matchResult; } - + /** + * @throws Exception + */ public function expectPartialMatchResult(PartialRouteResult $result) : self { if (! $this->route instanceof PartialRouteInterface) { @@ -123,6 +132,9 @@ public function expectPartialMatchResult(PartialRouteResult $result) : self return $this; } + /** + * @throws Exception + */ public function getExpectedPartialMatchResult() : PartialRouteResult { if (! $this->route instanceof PartialRouteInterface) { From f889b9535d546989b1390e0cd693ca9e22c9be08 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 15 Mar 2018 03:33:45 +1000 Subject: [PATCH 46/47] Update copyright headers --- src/ConfigProvider.php | 6 +++--- src/Container/RoutePluginManagerFactory.php | 6 +++--- src/Exception/DomainException.php | 6 +++--- src/Exception/ExceptionInterface.php | 6 +++--- src/Exception/InvalidArgumentException.php | 6 +++--- src/Exception/RuntimeException.php | 6 +++--- src/Module.php | 6 +++--- src/PriorityList.php | 6 +++--- src/Route/Chain.php | 6 +++--- src/Route/Hostname.php | 6 +++--- src/Route/Literal.php | 4 ++-- src/Route/Method.php | 6 +++--- src/Route/Part.php | 6 +++--- src/Route/PartialRouteTrait.php | 4 ++-- src/Route/Regex.php | 6 +++--- src/Route/Scheme.php | 6 +++--- src/Route/Segment.php | 6 +++--- src/RouteInterface.php | 6 +++--- src/RouteInvokableFactory.php | 6 +++--- src/RouteMatch.php | 6 +++--- src/RoutePluginManager.php | 6 +++--- src/RouteStackInterface.php | 6 +++--- src/SimpleRouteStack.php | 6 +++--- src/TreeRouteStack.php | 6 +++--- test/Container/RoutePluginManagerFactoryTest.php | 6 +++--- test/FactoryTester.php | 6 +++--- test/PriorityListTest.php | 6 +++--- test/Route/ChainTest.php | 4 ++-- test/Route/HostnameTest.php | 6 +++--- test/Route/LiteralTest.php | 4 ++-- test/Route/MethodTest.php | 6 +++--- test/Route/PartTest.php | 4 ++-- test/Route/RegexTest.php | 6 +++--- test/Route/SchemeTest.php | 6 +++--- test/Route/SegmentTest.php | 6 +++--- test/RouteMatchTest.php | 6 +++--- test/RoutePluginManagerTest.php | 6 +++--- test/SimpleRouteStackTest.php | 4 ++-- test/TestAsset/DummyRoute.php | 6 +++--- test/TestAsset/DummyRouteWithParam.php | 6 +++--- test/TreeRouteStackTest.php | 4 ++-- 41 files changed, 116 insertions(+), 116 deletions(-) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 39b35ba..8a0de62 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -1,8 +1,8 @@ Date: Thu, 29 Mar 2018 17:06:14 +1000 Subject: [PATCH 47/47] Add RouteConfigFactory factory --- src/ConfigProvider.php | 2 ++ src/Container/RouteConfigFactoryFactory.php | 22 ++++++++++++ .../RouteConfigFactoryFactoryTest.php | 34 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/Container/RouteConfigFactoryFactory.php create mode 100644 test/Container/RouteConfigFactoryFactoryTest.php diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 8a0de62..3e02776 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -9,6 +9,7 @@ namespace Zend\Router; +use Zend\Router\Container\RouteConfigFactoryFactory; use Zend\Router\Container\RoutePluginManagerFactory; /** @@ -46,6 +47,7 @@ public function getDependencyConfig() 'RoutePluginManager' => RoutePluginManager::class, ], 'factories' => [ + RouteConfigFactory::class => RouteConfigFactoryFactory::class, RoutePluginManager::class => RoutePluginManagerFactory::class, ], ]; diff --git a/src/Container/RouteConfigFactoryFactory.php b/src/Container/RouteConfigFactoryFactory.php new file mode 100644 index 0000000..f9f5f1c --- /dev/null +++ b/src/Container/RouteConfigFactoryFactory.php @@ -0,0 +1,22 @@ +get(RoutePluginManager::class)); + } +} diff --git a/test/Container/RouteConfigFactoryFactoryTest.php b/test/Container/RouteConfigFactoryFactoryTest.php new file mode 100644 index 0000000..275cb18 --- /dev/null +++ b/test/Container/RouteConfigFactoryFactoryTest.php @@ -0,0 +1,34 @@ +prophesize(ContainerInterface::class); + $container->get(RoutePluginManager::class) + ->willReturn(new RoutePluginManager(new ServiceManager())) + ->shouldBeCalled(); + $factory = new RouteConfigFactoryFactory(); + $service = $factory->__invoke($container->reveal()); + $this->assertInstanceOf(RouteConfigFactory::class, $service); + } +}