From f8d2edd2e1404074fb4f6e00500f12ecab3ebb99 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 11 Jan 2018 10:43:06 -0600 Subject: [PATCH] Backports middleware decorators from zendframework/zend-stratigility#134 This patch backports the following classes from the release-3.0.0 development series: - `Zend\Stratigility\Middleware\CallableMiddlewareDecorator`, for decorating middleware that follows the PSR-15 signature. - `Zend\Stratigility\Middleware\DoublePassMiddlewareDecorator`, for decorating callable middleware that follows the double pass middleware signature. - `Zend\Stratigility\Middleware\PathMiddlewareDecorator`, for providing path-segregated middleware. This required also extracting the class `Zend\Stratigility\Middleware\PathRequestHandlerDecorator`, which is used internally in order to decorate the request handler passed to middleware it decorates. All classes and their were updated to adapt to the http-middleware-compatibility shims. --- composer.json | 2 +- composer.lock | 403 +++++++----------- src/Exception/InvalidArgumentException.php | 16 + src/Exception/MissingResponseException.php | 10 + src/Exception/PathOutOfSyncException.php | 28 ++ .../CallableMiddlewareDecorator.php | 58 +++ .../DoublePassMiddlewareDecorator.php | 93 ++++ src/Middleware/PathMiddlewareDecorator.php | 144 +++++++ .../PathRequestHandlerDecorator.php | 53 +++ .../CallableMiddlewareDecoratorTest.php | 49 +++ .../DoublePassMiddlewareDecoratorTest.php | 92 ++++ .../PathMiddlewareDecoratorTest.php | 398 +++++++++++++++++ 12 files changed, 1099 insertions(+), 247 deletions(-) create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/PathOutOfSyncException.php create mode 100644 src/Middleware/CallableMiddlewareDecorator.php create mode 100644 src/Middleware/DoublePassMiddlewareDecorator.php create mode 100644 src/Middleware/PathMiddlewareDecorator.php create mode 100644 src/Middleware/PathRequestHandlerDecorator.php create mode 100644 test/Middleware/CallableMiddlewareDecoratorTest.php create mode 100644 test/Middleware/DoublePassMiddlewareDecoratorTest.php create mode 100644 test/Middleware/PathMiddlewareDecoratorTest.php diff --git a/composer.json b/composer.json index 5caf3da..7dd5740 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "require": { "php": "^5.6 || ^7.0", "psr/http-message": "^1.0", - "webimpress/http-middleware-compatibility": "^0.1.3", + "webimpress/http-middleware-compatibility": "^0.1.4", "zendframework/zend-escaper": "^2.3" }, "require-dev": { diff --git a/composer.lock b/composer.lock index f1ebba9..851c3c7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "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": "7a6e4e85ad33f59c95062ae46a20070c", + "content-hash": "303e45900c88a8f7f0866b9ba849ea70", "packages": [ { "name": "http-interop/http-middleware", - "version": "0.4.1", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/http-interop/http-middleware.git", - "reference": "9a801fe60e70d5d608b61d56b2dcde29516c81b9" + "reference": "b49e1f9f6c584e704317b563302e566b8ce11858" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/http-interop/http-middleware/zipball/9a801fe60e70d5d608b61d56b2dcde29516c81b9", - "reference": "9a801fe60e70d5d608b61d56b2dcde29516c81b9", + "url": "https://api.github.com/repos/http-interop/http-middleware/zipball/b49e1f9f6c584e704317b563302e566b8ce11858", + "reference": "b49e1f9f6c584e704317b563302e566b8ce11858", "shasum": "" }, "require": { @@ -32,7 +32,7 @@ }, "autoload": { "psr-4": { - "Interop\\Http\\ServerMiddleware\\": "src/" + "Interop\\Http\\Server\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -47,17 +47,16 @@ ], "description": "Common interface for HTTP server-side middleware", "keywords": [ - "factory", "http", "middleware", "psr", - "psr-17", + "psr-15", "psr-7", "request", "response" ], "abandoned": "http-interop/http-server-middleware", - "time": "2017-01-14T15:23:42+00:00" + "time": "2017-09-18T15:27:03+00:00" }, { "name": "psr/http-message", @@ -111,16 +110,16 @@ }, { "name": "webimpress/composer-extra-dependency", - "version": "0.2.0", + "version": "0.2.2", "source": { "type": "git", "url": "https://github.com/webimpress/composer-extra-dependency.git", - "reference": "97c241b2c80628a2cd1002cece3fc54c419b1776" + "reference": "31fa56391d30f03b1180c87610cbe22254780ad9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webimpress/composer-extra-dependency/zipball/97c241b2c80628a2cd1002cece3fc54c419b1776", - "reference": "97c241b2c80628a2cd1002cece3fc54c419b1776", + "url": "https://api.github.com/repos/webimpress/composer-extra-dependency/zipball/31fa56391d30f03b1180c87610cbe22254780ad9", + "reference": "31fa56391d30f03b1180c87610cbe22254780ad9", "shasum": "" }, "require": { @@ -153,29 +152,29 @@ "dependency", "webimpress" ], - "time": "2017-10-11T22:05:48+00:00" + "time": "2017-10-17T17:15:14+00:00" }, { "name": "webimpress/http-middleware-compatibility", - "version": "0.1.3", + "version": "0.1.4", "source": { "type": "git", "url": "https://github.com/webimpress/http-middleware-compatibility.git", - "reference": "5839f5abf54346eafa6617434bd5f618c0cc7adb" + "reference": "8ed1c2c7523dce0035b98bc4f3a73ca9cd1d3717" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webimpress/http-middleware-compatibility/zipball/5839f5abf54346eafa6617434bd5f618c0cc7adb", - "reference": "5839f5abf54346eafa6617434bd5f618c0cc7adb", + "url": "https://api.github.com/repos/webimpress/http-middleware-compatibility/zipball/8ed1c2c7523dce0035b98bc4f3a73ca9cd1d3717", + "reference": "8ed1c2c7523dce0035b98bc4f3a73ca9cd1d3717", "shasum": "" }, "require": { "http-interop/http-middleware": "^0.1.1 || ^0.2 || ^0.3 || ^0.4.1 || ^0.5", "php": "^5.6 || ^7.0", - "webimpress/composer-extra-dependency": "^0.2" + "webimpress/composer-extra-dependency": "^0.2.2" }, "require-dev": { - "phpunit/phpunit": "^5.7.22 || ^6.3.1" + "phpunit/phpunit": "^5.7.23 || ^6.4.3" }, "type": "library", "extra": { @@ -199,7 +198,7 @@ "psr-15", "webimpress" ], - "time": "2017-10-11T22:13:48+00:00" + "time": "2017-10-17T17:31:10+00:00" }, { "name": "zendframework/zend-escaper", @@ -303,25 +302,26 @@ }, { "name": "malukenho/docheader", - "version": "0.1.5", + "version": "0.1.7", "source": { "type": "git", "url": "https://github.com/malukenho/docheader.git", - "reference": "f35ada934d1de3f5ba09970a6541009a41347658" + "reference": "3eb59f0621125c0dc40775f1bcc3206c37993703" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/malukenho/docheader/zipball/f35ada934d1de3f5ba09970a6541009a41347658", - "reference": "f35ada934d1de3f5ba09970a6541009a41347658", + "url": "https://api.github.com/repos/malukenho/docheader/zipball/3eb59f0621125c0dc40775f1bcc3206c37993703", + "reference": "3eb59f0621125c0dc40775f1bcc3206c37993703", "shasum": "" }, "require": { "php": "~5.5|^7.0", - "symfony/console": "~2.0|^3.0", - "symfony/finder": "~2.0|^3.0" + "symfony/console": "~2.0 || ^3.0 || ^4.0", + "symfony/finder": "~2.0 || ^3.0 || ^4.0" }, "require-dev": { - "phpunit/phpunit": "^4.7" + "phpunit/phpunit": "^4.7", + "squizlabs/php_codesniffer": "^2.8" }, "bin": [ "bin/docheader" @@ -349,41 +349,44 @@ "code standard", "license" ], - "time": "2016-11-17T16:46:05+00:00" + "time": "2017-12-18T09:16:11+00:00" }, { "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", @@ -391,7 +394,7 @@ "object", "object graph" ], - "time": "2017-04-12T18:52:22+00:00" + "time": "2017-10-19T19:58:43+00:00" }, { "name": "phar-io/manifest", @@ -551,29 +554,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.1.1", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2" + "reference": "66465776cfc249844bde6d117abff1d22e06c2da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2d3d238c433cf69caeb4842e97a3223a116f94b2", - "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/66465776cfc249844bde6d117abff1d22e06c2da", + "reference": "66465776cfc249844bde6d117abff1d22e06c2da", "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\\": [ @@ -592,7 +601,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-27T17:38:31+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -643,16 +652,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": { @@ -664,7 +673,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": { @@ -702,20 +711,20 @@ "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": "5.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b" + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" }, "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/661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", "shasum": "" }, "require": { @@ -724,14 +733,13 @@ "php": "^7.0", "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": "^2.0.1", "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" }, "suggest": { @@ -740,7 +748,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.2.x-dev" + "dev-master": "5.3.x-dev" } }, "autoload": { @@ -755,7 +763,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -766,20 +774,20 @@ "testing", "xunit" ], - "time": "2017-08-03T12:40:43+00:00" + "time": "2017-12-06T09:29:45+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": { @@ -813,7 +821,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "time": "2017-11-27T13:52:08+00:00" }, { "name": "phpunit/php-text-template", @@ -907,16 +915,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0" + "reference": "791198a2c6254db10131eecfe8c06670700904db" }, "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/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", "shasum": "" }, "require": { @@ -952,20 +960,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-08-20T05:47:52+00:00" + "time": "2017-11-27T05:48:46+00:00" }, { "name": "phpunit/phpunit", - "version": "6.4.1", + "version": "6.5.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b770d8ba7e60295ee91d69d5a5e01ae833cac220" + "reference": "83d27937a310f2984fd575686138597147bdc7df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b770d8ba7e60295ee91d69d5a5e01ae833cac220", - "reference": "b770d8ba7e60295ee91d69d5a5e01ae833cac220", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/83d27937a310f2984fd575686138597147bdc7df", + "reference": "83d27937a310f2984fd575686138597147bdc7df", "shasum": "" }, "require": { @@ -979,12 +987,12 @@ "phar-io/version": "^1.0", "php": "^7.0", "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.2.2", - "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-code-coverage": "^5.3", + "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", + "phpunit/phpunit-mock-objects": "^5.0.5", + "sebastian/comparator": "^2.1", "sebastian/diff": "^2.0", "sebastian/environment": "^3.1", "sebastian/exporter": "^3.1", @@ -1010,7 +1018,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.4.x-dev" + "dev-master": "6.5.x-dev" } }, "autoload": { @@ -1036,33 +1044,33 @@ "testing", "xunit" ], - "time": "2017-10-07T17:53:53+00:00" + "time": "2017-12-17T06:31:19+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "4.0.4", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "2f789b59ab89669015ad984afa350c4ec577ade0" + "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" }, "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/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.5", "php": "^7.0", "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.0" + "sebastian/exporter": "^3.1" }, "conflict": { "phpunit/phpunit": "<6.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^6.5" }, "suggest": { "ext-soap": "*" @@ -1070,7 +1078,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "5.0.x-dev" } }, "autoload": { @@ -1085,7 +1093,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -1095,54 +1103,7 @@ "mock", "xunit" ], - "time": "2017-08-03T14:08:16+00:00" - }, - { - "name": "psr/log", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2018-01-06T05:45:45+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1191,30 +1152,30 @@ }, { "name": "sebastian/comparator", - "version": "2.0.2", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a" + "reference": "b11c729f95109b56a0fe9650c6a63a0fcd8c439f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/ae068fede81d06e7bb9bb46a367210a3d3e1fe6a", - "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b11c729f95109b56a0fe9650c6a63a0fcd8c439f", + "reference": "b11c729f95109b56a0fe9650c6a63a0fcd8c439f", "shasum": "" }, "require": { "php": "^7.0", "sebastian/diff": "^2.0", - "sebastian/exporter": "^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": { @@ -1245,13 +1206,13 @@ } ], "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": "2017-12-22T14:50:35+00:00" }, { "name": "sebastian/diff", @@ -1705,16 +1666,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "2.7.1", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "9b324f3a1132459a7274a0ace2e1b766ba80930f" + "reference": "dcbed1074f8244661eecddfc2a675430d8d33f62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9b324f3a1132459a7274a0ace2e1b766ba80930f", - "reference": "9b324f3a1132459a7274a0ace2e1b766ba80930f", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/dcbed1074f8244661eecddfc2a675430d8d33f62", + "reference": "dcbed1074f8244661eecddfc2a675430d8d33f62", "shasum": "" }, "require": { @@ -1779,43 +1740,48 @@ "phpcs", "standards" ], - "time": "2016-11-30T04:02:31+00:00" + "time": "2017-05-22T02:43:20+00:00" }, { "name": "symfony/console", - "version": "v3.2.2", + "version": "v4.0.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "4f9e449e76996adf310498a8ca955c6deebe29dd" + "reference": "fe0e69d7162cba0885791cf7eea5f0d7bc0f897e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/4f9e449e76996adf310498a8ca955c6deebe29dd", - "reference": "4f9e449e76996adf310498a8ca955c6deebe29dd", + "url": "https://api.github.com/repos/symfony/console/zipball/fe0e69d7162cba0885791cf7eea5f0d7bc0f897e", + "reference": "fe0e69d7162cba0885791cf7eea5f0d7bc0f897e", "shasum": "" }, "require": { - "php": ">=5.5.9", - "symfony/debug": "~2.8|~3.0", + "php": "^7.1.3", "symfony/polyfill-mbstring": "~1.0" }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, "require-dev": { "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/filesystem": "~2.8|~3.0", - "symfony/process": "~2.8|~3.0" + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0" }, "suggest": { "psr/log": "For using the console logger", "symfony/event-dispatcher": "", - "symfony/filesystem": "", + "symfony/lock": "", "symfony/process": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1842,86 +1808,29 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-01-08T20:47:33+00:00" - }, - { - "name": "symfony/debug", - "version": "v3.2.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "810ba5c1c5352a4ddb15d4719e8936751dff0b05" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/810ba5c1c5352a4ddb15d4719e8936751dff0b05", - "reference": "810ba5c1c5352a4ddb15d4719e8936751dff0b05", - "shasum": "" - }, - "require": { - "php": ">=5.5.9", - "psr/log": "~1.0" - }, - "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" - }, - "require-dev": { - "symfony/class-loader": "~2.8|~3.0", - "symfony/http-kernel": "~2.8|~3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Debug\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Debug Component", - "homepage": "https://symfony.com", - "time": "2017-01-02T20:32:22+00:00" + "time": "2018-01-03T07:38:00+00:00" }, { "name": "symfony/finder", - "version": "v3.2.2", + "version": "v4.0.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8c71141cae8e2957946b403cc71a67213c0380d6" + "reference": "8b08180f2b7ccb41062366b9ad91fbc4f1af8601" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8c71141cae8e2957946b403cc71a67213c0380d6", - "reference": "8c71141cae8e2957946b403cc71a67213c0380d6", + "url": "https://api.github.com/repos/symfony/finder/zipball/8b08180f2b7ccb41062366b9ad91fbc4f1af8601", + "reference": "8b08180f2b7ccb41062366b9ad91fbc4f1af8601", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^7.1.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1948,20 +1857,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-01-02T20:32:22+00:00" + "time": "2018-01-03T07:38:00+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.3.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", + "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", "shasum": "" }, "require": { @@ -1973,7 +1882,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -2007,7 +1916,7 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "theseer/tokenizer", @@ -2130,34 +2039,36 @@ }, { "name": "zendframework/zend-diactoros", - "version": "1.3.10", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-diactoros.git", - "reference": "83e8d98b9915de76c659ce27d683c02a0f99fa90" + "reference": "ed6ce7e2105c400ca10277643a8327957c0384b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/83e8d98b9915de76c659ce27d683c02a0f99fa90", - "reference": "83e8d98b9915de76c659ce27d683c02a0f99fa90", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/ed6ce7e2105c400ca10277643a8327957c0384b7", + "reference": "ed6ce7e2105c400ca10277643a8327957c0384b7", "shasum": "" }, "require": { - "php": "^5.4 || ^7.0", - "psr/http-message": "~1.0" + "php": "^5.6 || ^7.0", + "psr/http-message": "^1.0" }, "provide": { - "psr/http-message-implementation": "~1.0.0" + "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "^4.6 || ^5.5", - "zendframework/zend-coding-standard": "~1.0.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": "1.3-dev", - "dev-develop": "1.4-dev" + "dev-master": "1.7.x-dev", + "dev-develop": "1.8.x-dev" } }, "autoload": { @@ -2176,7 +2087,7 @@ "psr", "psr-7" ], - "time": "2017-01-23T04:53:24+00:00" + "time": "2018-01-04T18:21:48+00:00" } ], "aliases": [], diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..cc68d90 --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,16 @@ + + * function ( + * ServerRequestInterface $request, + * RequestHandlerInterface $handler + * ) : ResponseInterface + * + * + * such that it will operate as PSR-15 middleware. + * + * Neither the arguments nor the return value need be typehinted; however, if + * the signature is incompatible, a PHP Error will likely be thrown. + */ +class CallableMiddlewareDecorator implements MiddlewareInterface +{ + /** + * @var callable + */ + private $middleware; + + public function __construct(callable $middleware) + { + $this->middleware = $middleware; + } + + /** + * {@inheritDoc} + * @throws Exception\MissingResponseException if the decorated middleware + * fails to produce a response. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) + { + $response = ($this->middleware)($request, $handler); + if (! $response instanceof ResponseInterface) { + throw Exception\MissingResponseException::forCallableMiddleware($this->middleware); + } + return $response; + } +} diff --git a/src/Middleware/DoublePassMiddlewareDecorator.php b/src/Middleware/DoublePassMiddlewareDecorator.php new file mode 100644 index 0000000..921fbe9 --- /dev/null +++ b/src/Middleware/DoublePassMiddlewareDecorator.php @@ -0,0 +1,93 @@ + + * function ( + * ServerRequestInterface $request, + * ResponseInterface $response, + * callable $next + * ) : ResponseInterface + * + * + * such that it will operate as PSR-15 middleware. + * + * Neither the arguments nor the return value need be typehinted; however, if + * the signature is incompatible, a PHP Error will likely be thrown. + */ +class DoublePassMiddlewareDecorator implements MiddlewareInterface +{ + /** + * @var callable + */ + private $middleware; + + /** + * @var ResponseInterface + */ + private $responsePrototype; + + /** + * @throws Exception\MissingResponsePrototypeException if no response + * prototype is present, and zend-diactoros is not installed. + */ + public function __construct(callable $middleware, ResponseInterface $responsePrototype = null) + { + $this->middleware = $middleware; + + if (! $responsePrototype && ! class_exists(Response::class)) { + throw Exception\MissingResponsePrototypeException::create(); + } + + $this->responsePrototype = $responsePrototype ?: new Response(); + } + + /** + * {@inheritDoc} + * @throws Exception\MissingResponseException if the decorated middleware + * fails to produce a response. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) + { + $response = ($this->middleware)( + $request, + $this->responsePrototype, + $this->decorateHandler($handler) + ); + + if (! $response instanceof ResponseInterface) { + throw Exception\MissingResponseException::forCallableMiddleware($this->middleware); + } + + return $response; + } + + /** + * @return callable + */ + private function decorateHandler(RequestHandlerInterface $handler) + { + return function ($request, $response) use ($handler) { + return $handler->{HANDLER_METHOD}($request); + }; + } +} diff --git a/src/Middleware/PathMiddlewareDecorator.php b/src/Middleware/PathMiddlewareDecorator.php new file mode 100644 index 0000000..8ca4059 --- /dev/null +++ b/src/Middleware/PathMiddlewareDecorator.php @@ -0,0 +1,144 @@ +prefix = $this->normalizePrefix($prefix); + $this->middleware = $middleware; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) + { + $path = $request->getUri()->getPath(); + $path = $path ?: '/'; + + // Current path is shorter than decorator path + if (strlen($path) < strlen($this->prefix)) { + return $handler->{HANDLER_METHOD}($request); + } + + // Current path does not match decorator path + if (substr(strtolower($path), 0, strlen($this->prefix)) !== strtolower($this->prefix)) { + return $handler->{HANDLER_METHOD}($request); + } + + // Skip if match is not at a border ('/', '.', or end) + $border = $this->getBorder($path); + if ($border && '/' !== $border && '.' !== $border) { + return $handler->{HANDLER_METHOD}($request); + } + + // Trim off the part of the url that matches the prefix if it is non-empty + $requestToProcess = (! empty($this->prefix) && $this->prefix !== '/') + ? $this->prepareRequestWithTruncatedPrefix($request) + : $request; + + // Process our middleware. + // If the middleware calls on the handler, the handler should be provided + // the original request, as this indicates we've left the path-segregated + // layer. + return $this->middleware->process( + $requestToProcess, + new PathRequestHandlerDecorator($handler, $request) + ); + } + + /** + * @param string $path + * @return string + */ + private function getBorder($path) + { + if ($this->prefix === '/') { + return '/'; + } + + $length = strlen($this->prefix); + return strlen($path) > $length ? $path[$length] : ''; + } + + /** + * @return ServerRequestInterface + */ + private function prepareRequestWithTruncatedPrefix(ServerRequestInterface $request) + { + $uri = $request->getUri(); + $path = $this->getTruncatedPath($this->prefix, $uri->getPath()); + $new = $uri->withPath($path); + return $request->withUri($new); + } + + /** + * @param string $segment + * @param string $path + * @return string + */ + private function getTruncatedPath($segment, $path) + { + if ($segment === $path) { + // Decorated path and current path are the same; return empty string + return ''; + } + + $length = strlen($segment); + if (strlen($path) > $length) { + // Strip decorated path from start of current path + return substr($path, $length); + } + + if ('/' === substr($segment, -1)) { + // Re-try by submitting with / stripped from end of segment + return $this->getTruncatedPath(rtrim($segment, '/'), $path); + } + + // Segment is longer than path; this is a problem. + throw Exception\PathOutOfSyncException::forPath($this->prefix, $path); + } + + /** + * Ensures that the right-most slash is trimmed for prefixes of more than + * one character, and that the prefix begins with a slash. + * + * @param string $prefix + * @return string + */ + private function normalizePrefix($prefix) + { + $prefix = strlen($prefix) > 1 ? rtrim($prefix, '/') : $prefix; + if ('/' !== substr($prefix, 0, 1)) { + $prefix = '/' . $prefix; + } + return $prefix; + } +} diff --git a/src/Middleware/PathRequestHandlerDecorator.php b/src/Middleware/PathRequestHandlerDecorator.php new file mode 100644 index 0000000..4c77a4b --- /dev/null +++ b/src/Middleware/PathRequestHandlerDecorator.php @@ -0,0 +1,53 @@ +handler = $handler; + $this->originalRequest = $originalRequest; + } + + /** + * Invokes the composed handler with the original server request. + * {@inheritDocs} + */ + public function handle(ServerRequestInterface $request) + { + return $this->handler->{HANDLER_METHOD}($this->originalRequest); + } +} diff --git a/test/Middleware/CallableMiddlewareDecoratorTest.php b/test/Middleware/CallableMiddlewareDecoratorTest.php new file mode 100644 index 0000000..e74991a --- /dev/null +++ b/test/Middleware/CallableMiddlewareDecoratorTest.php @@ -0,0 +1,49 @@ +prophesize(ServerRequestInterface::class)->reveal(); + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); + + $middleware = function ($request, $handler) { + return 'foo'; + }; + + $decorator = new CallableMiddlewareDecorator($middleware); + + $this->expectException(Exception\MissingResponseException::class); + $this->expectExceptionMessage('failed to produce a response'); + $decorator->process($request, $handler); + } + + public function testCallableMiddlewareReturningAResponseSucceedsProcessCall() + { + $request = $this->prophesize(ServerRequestInterface::class)->reveal(); + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); + $response = $this->prophesize(ResponseInterface::class)->reveal(); + + $middleware = function ($request, $handler) use ($response) { + return $response; + }; + + $decorator = new CallableMiddlewareDecorator($middleware); + + $this->assertSame($response, $decorator->process($request, $handler)); + } +} diff --git a/test/Middleware/DoublePassMiddlewareDecoratorTest.php b/test/Middleware/DoublePassMiddlewareDecoratorTest.php new file mode 100644 index 0000000..4500326 --- /dev/null +++ b/test/Middleware/DoublePassMiddlewareDecoratorTest.php @@ -0,0 +1,92 @@ +prophesize(ResponseInterface::class)->reveal(); + $request = $this->prophesize(ServerRequestInterface::class)->reveal(); + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); + + $middleware = function ($request, $response, $next) { + return 'foo'; + }; + + $decorator = new DoublePassMiddlewareDecorator($middleware, $response); + + $this->expectException(Exception\MissingResponseException::class); + $this->expectExceptionMessage('failed to produce a response'); + $decorator->process($request, $handler); + } + + public function testCallableMiddlewareReturningAResponseSucceedsProcessCall() + { + $response = $this->prophesize(ResponseInterface::class)->reveal(); + $request = $this->prophesize(ServerRequestInterface::class)->reveal(); + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); + + $middleware = function ($request, $response, $next) { + return $response; + }; + + $decorator = new DoublePassMiddlewareDecorator($middleware, $response); + + $this->assertSame($response, $decorator->process($request, $handler)); + } + + public function testCallableMiddlewareCanDelegateViaHandler() + { + $response = $this->prophesize(ResponseInterface::class); + $request = $this->prophesize(ServerRequestInterface::class); + + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler + ->{HANDLER_METHOD}(Argument::that([$request, 'reveal'])) + ->will([$response, 'reveal']); + + $middleware = function ($request, $response, $next) { + return $next($request, $response); + }; + + $decorator = new DoublePassMiddlewareDecorator($middleware, $response->reveal()); + + $this->assertSame( + $response->reveal(), + $decorator->process($request->reveal(), $handler->reveal()) + ); + } + + public function testDecoratorCreatesAResponsePrototypeIfNoneIsProvided() + { + $request = $this->prophesize(ServerRequestInterface::class)->reveal(); + $handler = $this->prophesize(RequestHandlerInterface::class)->reveal(); + + $middleware = function ($request, $response, $next) { + return $response; + }; + + $decorator = new DoublePassMiddlewareDecorator($middleware); + + $response = $decorator->process($request, $handler); + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertInstanceOf(Response::class, $response); + } +} diff --git a/test/Middleware/PathMiddlewareDecoratorTest.php b/test/Middleware/PathMiddlewareDecoratorTest.php new file mode 100644 index 0000000..c8e629b --- /dev/null +++ b/test/Middleware/PathMiddlewareDecoratorTest.php @@ -0,0 +1,398 @@ +uri = $this->prophesize(UriInterface::class); + $this->request = $this->prophesize(ServerRequestInterface::class); + $this->response = $this->prophesize(ResponseInterface::class); + $this->handler = $this->prophesize(RequestHandlerInterface::class); + $this->toDecorate = $this->prophesize(MiddlewareInterface::class); + } + + public function testImplementsMiddlewareInterface() + { + $middleware = new PathMiddlewareDecorator('/foo', $this->toDecorate->reveal()); + $this->assertInstanceOf(MiddlewareInterface::class, $middleware); + } + + public function testComposesMiddlewarePassedToConstructor() + { + $toDecorate = $this->prophesize(MiddlewareInterface::class)->reveal(); + $middleware = new PathMiddlewareDecorator('/foo', $toDecorate); + $this->assertAttributeSame($toDecorate, 'middleware', $middleware); + } + + public function testComposesPathPrefixPassedToConstructor() + { + $middleware = new PathMiddlewareDecorator('/foo', $this->toDecorate->reveal()); + $this->assertAttributeSame('/foo', 'prefix', $middleware); + } + + public function testEmptyPathPrefixBecomesSingleSlash() + { + $middleware = new PathMiddlewareDecorator('', $this->toDecorate->reveal()); + $this->assertAttributeSame('/', 'prefix', $middleware); + } + + public function testTrailingSlashIsStrippedFromPathPrefix() + { + $middleware = new PathMiddlewareDecorator('/foo/', $this->toDecorate->reveal()); + $this->assertAttributeSame('/foo', 'prefix', $middleware); + } + + public function testAddsLeadingSlashWhenMissingFromPathPrefix() + { + $middleware = new PathMiddlewareDecorator('foo', $this->toDecorate->reveal()); + $this->assertAttributeSame('/foo', 'prefix', $middleware); + } + + public function testDelegatesOriginalRequestToHandlerIfRequestPathIsShorterThanDecoratorPrefix() + { + $this->uri + ->getPath() + ->willReturn('/f'); + $this->request + ->getUri() + ->will([$this->uri, 'reveal']); + $this->handler + ->{HANDLER_METHOD}(Argument::that([$this->request, 'reveal'])) + ->will([$this->response, 'reveal']); + + $this->toDecorate->process(Argument::any())->shouldNotBeCalled(); + + $middleware = new PathMiddlewareDecorator('/foo', $this->toDecorate->reveal()); + + $this->assertSame( + $this->response->reveal(), + $middleware->process($this->request->reveal(), $this->handler->reveal()) + ); + } + + public function testDelegatesOriginalRequestToHandlerIfRequestPathIsDoesNotMatchDecoratorPath() + { + $this->uri + ->getPath() + ->willReturn('/bar'); + $this->request + ->getUri() + ->will([$this->uri, 'reveal']); + $this->handler + ->{HANDLER_METHOD}(Argument::that([$this->request, 'reveal'])) + ->will([$this->response, 'reveal']); + + $this->toDecorate->process(Argument::any())->shouldNotBeCalled(); + + $middleware = new PathMiddlewareDecorator('/foo', $this->toDecorate->reveal()); + } + + public function testDelegatesOrignalRequestToHandlerIfRequestDoesNotMatchPrefixAtABoundary() + { + // e.g., if route is "/foo", but path is "/foobar", no match + $uri = (new Uri())->withPath('/foobar'); + $request = (new ServerRequest())->withUri($uri); + $response = new Response(); + + $middleware = $this->prophesize(MiddlewareInterface::class); + $middleware->process(Argument::any(), Argument::any())->shouldNotBeCalled(); + + $decorator = new PathMiddlewareDecorator('/foo', $middleware->reveal()); + + $handler = $this->prophesize(RequestHandlerInterface::class); + $handler->{HANDLER_METHOD}($request)->willReturn($response); + + $this->assertSame( + $response, + $decorator->process($request, $handler->reveal()) + ); + } + + public function nestedPathCombinations() + { + return [ + // name => [$prefix, $nestPrefix, $uriPath, $expectsHeader ] + 'empty-bare-bare' => ['', 'foo', '/foo', true], + 'empty-bare-bareplus' => ['', 'foo', '/foobar', false], + 'empty-bare-tail' => ['', 'foo', '/foo/', true], + 'empty-bare-tailplus' => ['', 'foo', '/foo/bar', true], + 'empty-tail-bare' => ['', 'foo/', '/foo', true], + 'empty-tail-bareplus' => ['', 'foo/', '/foobar', false], + 'empty-tail-tail' => ['', 'foo/', '/foo/', true], + 'empty-tail-tailplus' => ['', 'foo/', '/foo/bar', true], + 'empty-prefix-bare' => ['', '/foo', '/foo', true], + 'empty-prefix-bareplus' => ['', '/foo', '/foobar', false], + 'empty-prefix-tail' => ['', '/foo', '/foo/', true], + 'empty-prefix-tailplus' => ['', '/foo', '/foo/bar', true], + 'empty-surround-bare' => ['', '/foo/', '/foo', true], + 'empty-surround-bareplus' => ['', '/foo/', '/foobar', false], + 'empty-surround-tail' => ['', '/foo/', '/foo/', true], + 'empty-surround-tailplus' => ['', '/foo/', '/foo/bar', true], + 'root-bare-bare' => ['/', 'foo', '/foo', true], + 'root-bare-bareplus' => ['/', 'foo', '/foobar', false], + 'root-bare-tail' => ['/', 'foo', '/foo/', true], + 'root-bare-tailplus' => ['/', 'foo', '/foo/bar', true], + 'root-tail-bare' => ['/', 'foo/', '/foo', true], + 'root-tail-bareplus' => ['/', 'foo/', '/foobar', false], + 'root-tail-tail' => ['/', 'foo/', '/foo/', true], + 'root-tail-tailplus' => ['/', 'foo/', '/foo/bar', true], + 'root-prefix-bare' => ['/', '/foo', '/foo', true], + 'root-prefix-bareplus' => ['/', '/foo', '/foobar', false], + 'root-prefix-tail' => ['/', '/foo', '/foo/', true], + 'root-prefix-tailplus' => ['/', '/foo', '/foo/bar', true], + 'root-surround-bare' => ['/', '/foo/', '/foo', true], + 'root-surround-bareplus' => ['/', '/foo/', '/foobar', false], + 'root-surround-tail' => ['/', '/foo/', '/foo/', true], + 'root-surround-tailplus' => ['/', '/foo/', '/foo/bar', true], + 'bare-bare-bare' => ['foo', 'bar', '/foo/bar', true], + 'bare-bare-bareplus' => ['foo', 'bar', '/foo/barbaz', false], + 'bare-bare-tail' => ['foo', 'bar', '/foo/bar/', true], + 'bare-bare-tailplus' => ['foo', 'bar', '/foo/bar/baz', true], + 'bare-tail-bare' => ['foo', 'bar/', '/foo/bar', true], + 'bare-tail-bareplus' => ['foo', 'bar/', '/foo/barbaz', false], + 'bare-tail-tail' => ['foo', 'bar/', '/foo/bar/', true], + 'bare-tail-tailplus' => ['foo', 'bar/', '/foo/bar/baz', true], + 'bare-prefix-bare' => ['foo', '/bar', '/foo/bar', true], + 'bare-prefix-bareplus' => ['foo', '/bar', '/foo/barbaz', false], + 'bare-prefix-tail' => ['foo', '/bar', '/foo/bar/', true], + 'bare-prefix-tailplus' => ['foo', '/bar', '/foo/bar/baz', true], + 'bare-surround-bare' => ['foo', '/bar/', '/foo/bar', true], + 'bare-surround-bareplus' => ['foo', '/bar/', '/foo/barbaz', false], + 'bare-surround-tail' => ['foo', '/bar/', '/foo/bar/', true], + 'bare-surround-tailplus' => ['foo', '/bar/', '/foo/bar/baz', true], + 'tail-bare-bare' => ['foo/', 'bar', '/foo/bar', true], + 'tail-bare-bareplus' => ['foo/', 'bar', '/foo/barbaz', false], + 'tail-bare-tail' => ['foo/', 'bar', '/foo/bar/', true], + 'tail-bare-tailplus' => ['foo/', 'bar', '/foo/bar/baz', true], + 'tail-tail-bare' => ['foo/', 'bar/', '/foo/bar', true], + 'tail-tail-bareplus' => ['foo/', 'bar/', '/foo/barbaz', false], + 'tail-tail-tail' => ['foo/', 'bar/', '/foo/bar/', true], + 'tail-tail-tailplus' => ['foo/', 'bar/', '/foo/bar/baz', true], + 'tail-prefix-bare' => ['foo/', '/bar', '/foo/bar', true], + 'tail-prefix-bareplus' => ['foo/', '/bar', '/foo/barbaz', false], + 'tail-prefix-tail' => ['foo/', '/bar', '/foo/bar/', true], + 'tail-prefix-tailplus' => ['foo/', '/bar', '/foo/bar/baz', true], + 'tail-surround-bare' => ['foo/', '/bar/', '/foo/bar', true], + 'tail-surround-bareplus' => ['foo/', '/bar/', '/foo/barbaz', false], + 'tail-surround-tail' => ['foo/', '/bar/', '/foo/bar/', true], + 'tail-surround-tailplus' => ['foo/', '/bar/', '/foo/bar/baz', true], + 'prefix-bare-bare' => ['/foo', 'bar', '/foo/bar', true], + 'prefix-bare-bareplus' => ['/foo', 'bar', '/foo/barbaz', false], + 'prefix-bare-tail' => ['/foo', 'bar', '/foo/bar/', true], + 'prefix-bare-tailplus' => ['/foo', 'bar', '/foo/bar/baz', true], + 'prefix-tail-bare' => ['/foo', 'bar/', '/foo/bar', true], + 'prefix-tail-bareplus' => ['/foo', 'bar/', '/foo/barbaz', false], + 'prefix-tail-tail' => ['/foo', 'bar/', '/foo/bar/', true], + 'prefix-tail-tailplus' => ['/foo', 'bar/', '/foo/bar/baz', true], + 'prefix-prefix-bare' => ['/foo', '/bar', '/foo/bar', true], + 'prefix-prefix-bareplus' => ['/foo', '/bar', '/foo/barbaz', false], + 'prefix-prefix-tail' => ['/foo', '/bar', '/foo/bar/', true], + 'prefix-prefix-tailplus' => ['/foo', '/bar', '/foo/bar/baz', true], + 'prefix-surround-bare' => ['/foo', '/bar/', '/foo/bar', true], + 'prefix-surround-bareplus' => ['/foo', '/bar/', '/foo/barbaz', false], + 'prefix-surround-tail' => ['/foo', '/bar/', '/foo/bar/', true], + 'prefix-surround-tailplus' => ['/foo', '/bar/', '/foo/bar/baz', true], + 'surround-bare-bare' => ['/foo/', 'bar', '/foo/bar', true], + 'surround-bare-bareplus' => ['/foo/', 'bar', '/foo/barbaz', false], + 'surround-bare-tail' => ['/foo/', 'bar', '/foo/bar/', true], + 'surround-bare-tailplus' => ['/foo/', 'bar', '/foo/bar/baz', true], + 'surround-tail-bare' => ['/foo/', 'bar/', '/foo/bar', true], + 'surround-tail-bareplus' => ['/foo/', 'bar/', '/foo/barbaz', false], + 'surround-tail-tail' => ['/foo/', 'bar/', '/foo/bar/', true], + 'surround-tail-tailplus' => ['/foo/', 'bar/', '/foo/bar/baz', true], + 'surround-prefix-bare' => ['/foo/', '/bar', '/foo/bar', true], + 'surround-prefix-bareplus' => ['/foo/', '/bar', '/foo/barbaz', false], + 'surround-prefix-tail' => ['/foo/', '/bar', '/foo/bar/', true], + 'surround-prefix-tailplus' => ['/foo/', '/bar', '/foo/bar/baz', true], + 'surround-surround-bare' => ['/foo/', '/bar/', '/foo/bar', true], + 'surround-surround-bareplus' => ['/foo/', '/bar/', '/foo/barbaz', false], + 'surround-surround-tail' => ['/foo/', '/bar/', '/foo/bar/', true], + 'surround-surround-tailplus' => ['/foo/', '/bar/', '/foo/bar/baz', true], + ]; + } + + /** + * @dataProvider nestedPathCombinations + */ + public function testNestedMiddlewareOnlyMatchesAtPathBoundaries( + string $prefix, + string $nestPrefix, + string $uriPath, + bool $expectsHeader + ) { + $finalHandler = $this->prophesize(RequestHandlerInterface::class); + $finalHandler->{HANDLER_METHOD}(Argument::any())->willReturn(new Response()); + + $nested = new PathMiddlewareDecorator($nestPrefix, new class () implements MiddlewareInterface { + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ) : ResponseInterface { + return (new Response())->withHeader('X-Found', 'true'); + } + }); + + $topLevel = new PathMiddlewareDecorator($prefix, new class ($nested) implements MiddlewareInterface { + private $middleware; + + public function __construct(MiddlewareInterface $middleware) + { + $this->middleware = $middleware; + } + + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ) : ResponseInterface { + return $this->middleware->process($request, $handler); + } + }); + + $uri = (new Uri())->withPath($uriPath); + $request = (new ServerRequest())->withUri($uri); + + $response = $topLevel->process($request, $finalHandler->reveal()); + + $this->assertSame( + $expectsHeader, + $response->hasHeader('X-Found'), + sprintf( + 'Failed with full path %s against top-level prefix "%s" and nested prefix "%s"; expected %s', + $uriPath, + $prefix, + $nestPrefix, + var_export($expectsHeader, true) + ) + ); + } + + public function rootPathsProvider() + { + return [ + 'empty' => [''], + 'root' => ['/'], + ]; + } + + /** + * @group matching + * @dataProvider rootPathsProvider + * + * @param string $path + */ + public function testTreatsBothSlashAndEmptyPathAsTheRootPath($path) + { + $finalHandler = $this->prophesize(RequestHandlerInterface::class); + $finalHandler->{HANDLER_METHOD}(Argument::any())->willReturn(new Response()); + + $middleware = new PathMiddlewareDecorator($path, new class () implements MiddlewareInterface { + public function process(ServerRequestInterface $req, RequestHandlerInterface $handler) : ResponseInterface + { + $res = new Response(); + return $res->withHeader('X-Found', 'true'); + } + }); + $uri = (new Uri())->withPath($path); + $request = (new ServerRequest)->withUri($uri); + + $response = $middleware->process($request, $finalHandler->reveal()); + $this->assertTrue($response->hasHeader('x-found')); + } + + public function testRequestPathPassedToDecoratedMiddlewareTrimsPathPrefix() + { + $finalHandler = $this->prophesize(RequestHandlerInterface::class); + $finalHandler->{HANDLER_METHOD}(Argument::any())->willReturn(new Response()); + + $request = new ServerRequest([], [], 'http://local.example.com/foo/bar', 'GET', 'php://memory'); + + $middleware = $this->prophesize(MiddlewareInterface::class); + $middleware + ->process( + Argument::that(function (ServerRequestInterface $req) { + Assert::assertSame('/bar', $req->getUri()->getPath()); + + return true; + }), + Argument::any() + ) + ->willReturn(new Response()) + ->shouldBeCalledTimes(1); + + $decorator = new PathMiddlewareDecorator('/foo', $middleware->reveal()); + $decorator->process($request, $finalHandler->reveal()); + } + + public function testInvocationOfHandlerByDecoratedMiddlewareWillInvokeWithOriginalRequestPath() + { + $request = new ServerRequest([], [], 'http://local.example.com/test', 'GET', 'php://memory'); + $expectedResponse = new Response(); + + $finalHandler = $this->prophesize(RequestHandlerInterface::class); + $finalHandler + ->{HANDLER_METHOD}(Argument::that(function ($received) use ($request) { + Assert::assertNotSame( + $request, + $received, + 'Final handler received same request, and should not have' + ); + + Assert::assertSame( + $request->getUri()->getPath(), + $received->getUri()->getPath(), + 'Final handler received request with different path' + ); + + return $received; + })) + ->willReturn($expectedResponse); + + $segregatedMiddleware = $this->prophesize(MiddlewareInterface::class); + $segregatedMiddleware + ->process( + Argument::that(function ($received) use ($request) { + Assert::assertNotSame( + $request, + $received, + 'Segregated middleware received same request as decorator, but should not have' + ); + return $received; + }), + Argument::type(RequestHandlerInterface::class) + ) + ->will(function ($args) { + $request = $args[0]; + $next = $args[1]; + return $next->{HANDLER_METHOD}($request); + }); + + $decoratedMiddleware = new PathMiddlewareDecorator('/test', $segregatedMiddleware->reveal()); + + $this->assertSame( + $expectedResponse, + $decoratedMiddleware->process($request, $finalHandler->reveal()) + ); + } +}