diff --git a/.gitattributes b/.gitattributes index 7ae0617..1fd8ce1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,12 @@ -.editorconfig export-ignore -.gitattributes export-ignore -.github/ export-ignore -.gitignore export-ignore -.php_cs export-ignore -.scrutinizer.yml export-ignore -.styleci.yml export-ignore -.travis.yml export-ignore -phpspec.yml.ci export-ignore -phpspec.yml.dist export-ignore -phpunit.xml.dist export-ignore -spec/ export-ignore -tests/ export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +/.github/ export-ignore +.gitignore export-ignore +/.php_cs.dist export-ignore +/phpspec.ci.yml export-ignore +/phpspec.yml.dist export-ignore +/phpstan-baseline.neon export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/spec/ export-ignore +/tests/ export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index fb288d9..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributing - -Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index d4ecf20..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,27 +0,0 @@ -| Q | A -| ------------ | --- -| Bug? | no|yes -| New Feature? | no|yes -| Version | Specific version or SHA of a commit - - -#### Actual Behavior - -What is the actual behavior? - - -#### Expected Behavior - -What is the behavior you expect? - - -#### Steps to Reproduce - -What are the steps to reproduce this bug? Please add code examples, -screenshots or links to GitHub repositories that reproduce the problem. - - -#### Possible Solutions - -If you have already ideas how to solve the issue, add them here. -(remove this section if not needed) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 323987b..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,43 +0,0 @@ -| Q | A -| --------------- | --- -| Bug fix? | no|yes -| New feature? | no|yes -| BC breaks? | no|yes -| Deprecations? | no|yes -| Related tickets | fixes #X, partially #Y, mentioned in #Z -| Documentation | if this is a new feature, link to pull request in https://github.com/php-http/documentation that adds relevant documentation -| License | MIT - - -#### What's in this PR? - -Explain what the changes in this PR do. - - -#### Why? - -Which problem does the PR fix? (remove this section if you linked an issue above) - - -#### Example Usage - -``` php -// If you added new features, show examples of how to use them here -// (remove this section if not a new feature) - -$foo = new Foo(); - -// Now we can do -$foo->doSomething(); -``` - - -#### Checklist - -- [ ] Updated CHANGELOG.md to describe BC breaks / deprecations | new feature | bugfix -- [ ] Documentation pull request created (if not simply a bugfix) - - -#### To Do - -- [ ] If the PR is not complete but you want to discuss the approach, list what remains to be done here diff --git a/.github/workflows/.editorconfig b/.github/workflows/.editorconfig new file mode 100644 index 0000000..7bd3346 --- /dev/null +++ b/.github/workflows/.editorconfig @@ -0,0 +1,2 @@ +[*.yml] +indent_size = 2 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..1979b2e --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,19 @@ +name: Checks + +on: + push: + branches: + - '*.x' + pull_request: + +jobs: + composer-normalize: + name: Composer Normalize + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Composer normalize + uses: docker://ergebnis/composer-normalize-action diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..fe574a0 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,36 @@ +name: Static analysis + +on: + push: + branches: + - '*.x' + pull_request: + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: PHPStan + uses: docker://oskarstark/phpstan-ga + env: + REQUIRE_DEV: true + with: + args: analyze --no-progress + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: PHP-CS-Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --dry-run --diff diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..be2c811 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,110 @@ +name: tests + +on: + push: + branches: + - '*.x' + pull_request: + +jobs: + latest: + name: PHP ${{ matrix.php }} Latest + runs-on: ubuntu-latest + strategy: + matrix: + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Emulate PHP 8.3 + run: composer config platform.php 8.3.999 + if: matrix.php == '8.4' + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + coverage: none + + - name: Install dependencies + run: composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: composer test + + psr-7_2: + name: PHP PSR-7 2.0 + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + tools: composer + coverage: none + + - name: Install dependencies + run: | + rm src/MessageFactory/SlimMessageFactory.php src/StreamFactory/SlimStreamFactory.php src/UriFactory/SlimUriFactory.php spec/MessageFactory/SlimMessageFactorySpec.php spec/StreamFactory/SlimStreamFactorySpec.php spec/UriFactory/SlimUriFactorySpec.php + composer remove --dev "slim/slim" --no-interaction --no-update + composer require "psr/http-message:^2.0" --no-interaction --no-update + composer update --prefer-dist --prefer-stable --no-interaction --no-progress + + - name: Execute tests + run: composer test + + lowest: + name: PHP ${{ matrix.php }} Lowest + runs-on: ubuntu-latest + strategy: + matrix: + php: ['7.2', '7.3', '7.4'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + coverage: none + + - name: Install dependencies + run: | + composer require "sebastian/comparator:^3.0.2" --no-interaction --no-update + composer update --prefer-dist --prefer-stable --prefer-lowest --no-interaction --no-progress + + - name: Execute tests + run: composer test + + coverage: + name: Code Coverage + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + tools: composer + coverage: xdebug + + - name: Install dependencies + run: | + composer require "friends-of-phpspec/phpspec-code-coverage:^4.3.2" --no-interaction --no-update + composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: composer test-ci diff --git a/.gitignore b/.gitignore index da734f1..fddbbfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -.puli/ -build/ -vendor/ -composer.lock -phpspec.yml -phpunit.xml +.php-cs-fixer.php +.php-cs-fixer.cache +/build/ +/composer.lock +/phpspec.yml +/phpstan.neon +/phpunit.xml +/vendor/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..55bd8ed --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,19 @@ +in(__DIR__.'/src') + ->in(__DIR__.'/spec') + ->name('*.php') +; + +$config = new PhpCsFixer\Config(); + +return $config + ->setRiskyAllowed(true) + ->setRules([ + '@Symfony' => true, + 'single_line_throw' => false, + 'trailing_comma_in_multiline' => false, // for methods this is incompatible with PHP 7 + ]) + ->setFinder($finder) +; diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 23ba165..0000000 --- a/.php_cs +++ /dev/null @@ -1,13 +0,0 @@ -=5.4", - "psr/http-message": "^1.0", - "php-http/message-factory": "^1.0.2", - "clue/stream-filter": "^1.3" + "php": "^7.2 || ^8.0", + "clue/stream-filter": "^1.5", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" }, "require-dev": { - "zendframework/zend-diactoros": "^1.0", - "guzzlehttp/psr7": "^1.0", "ext-zlib": "*", - "phpspec/phpspec": "^2.4", - "henrikbjorn/phpspec-code-coverage" : "^1.0", - "coduo/phpspec-data-provider-extension": "^1.0" + "ergebnis/composer-normalize": "^2.6", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0" }, "suggest": { - "zendframework/zend-diactoros": "Used with Diactoros Factories", + "ext-zlib": "Used with compressor/decompressor streams", "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", - "ext-zlib": "Used with compressor/decompressor streams" + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "ergebnis/composer-normalize": true + } }, "autoload": { "psr-4": { @@ -44,11 +58,6 @@ }, "scripts": { "test": "vendor/bin/phpspec run", - "test-ci": "vendor/bin/phpspec run -c phpspec.yml.ci" - }, - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } + "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" } } diff --git a/phpspec.yml.ci b/phpspec.ci.yml similarity index 64% rename from phpspec.yml.ci rename to phpspec.ci.yml index f08e148..5f8a626 100644 --- a/phpspec.yml.ci +++ b/phpspec.ci.yml @@ -4,8 +4,8 @@ suites: psr4_prefix: Http\Message formatter.name: pretty extensions: - - PhpSpec\Extension\CodeCoverageExtension - - Coduo\PhpSpec\DataProvider\DataProviderExtension + FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: ~ code_coverage: format: clover output: build/coverage.xml +runner.maintainers.errors.level: 16383 diff --git a/phpspec.yml.dist b/phpspec.yml.dist index 8d38a4e..785cf0e 100644 --- a/phpspec.yml.dist +++ b/phpspec.yml.dist @@ -3,5 +3,4 @@ suites: namespace: Http\Message psr4_prefix: Http\Message formatter.name: pretty -extensions: - - Coduo\PhpSpec\DataProvider\DataProviderExtension +runner.maintainers.errors.level: 16383 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..31bd899 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,186 @@ +parameters: + ignoreErrors: + - + message: "#^Property Http\\\\Message\\\\Authentication\\\\QueryParam\\:\\:\\$params type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Authentication/QueryParam.php + + - + message: "#^Method Http\\\\Message\\\\Authentication\\\\QueryParam\\:\\:__construct\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Authentication/QueryParam.php + + - + message: "#^Parameter \\#1 \\$statusLine of method Http\\\\Message\\\\Builder\\\\ResponseBuilder\\:\\:setStatus\\(\\) expects string, string\\|null given\\.$#" + count: 1 + path: src/Builder/ResponseBuilder.php + + - + message: "#^Result of \\|\\| is always true\\.$#" + count: 1 + path: src/Builder/ResponseBuilder.php + + - + message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#" + count: 1 + path: src/Builder/ResponseBuilder.php + + - + message: "#^Method Http\\\\Message\\\\Cookie\\:\\:createWithoutValidation\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Cookie.php + + - + message: "#^Parameter \\#2 \\$string2 of function strcasecmp expects string, string\\|null given\\.$#" + count: 1 + path: src/Cookie.php + + - + message: "#^Parameter \\#1 \\$str of function preg_quote expects string, string\\|null given\\.$#" + count: 1 + path: src/Cookie.php + + - + message: "#^Method Http\\\\Message\\\\Cookie\\:\\:validateName\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Cookie.php + + - + message: "#^Method Http\\\\Message\\\\Cookie\\:\\:validateValue\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Cookie.php + + - + message: "#^Method Http\\\\Message\\\\Cookie\\:\\:validateMaxAge\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Cookie.php + + - + message: "#^Method Http\\\\Message\\\\Cookie\\:\\:normalizeDomain\\(\\) should return string but returns string\\|null\\.$#" + count: 1 + path: src/Cookie.php + + - + message: "#^Class Http\\\\Message\\\\CookieJar implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Method Http\\\\Message\\\\CookieJar\\:\\:addCookie\\(\\) has no return type specified\\.$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Method Http\\\\Message\\\\CookieJar\\:\\:removeCookie\\(\\) has no return type specified\\.$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Method Http\\\\Message\\\\CookieJar\\:\\:setCookies\\(\\) has no return type specified\\.$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Method Http\\\\Message\\\\CookieJar\\:\\:addCookies\\(\\) has no return type specified\\.$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Method Http\\\\Message\\\\CookieJar\\:\\:removeCookies\\(\\) has no return type specified\\.$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Method Http\\\\Message\\\\CookieJar\\:\\:removeMatchingCookies\\(\\) has no return type specified\\.$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Left side of && is always true\\.$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Method Http\\\\Message\\\\CookieJar\\:\\:clear\\(\\) has no return type specified\\.$#" + count: 1 + path: src/CookieJar.php + + - + message: "#^Property Http\\\\Message\\\\CookieUtil\\:\\:\\$dateFormats type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/CookieUtil.php + + - + message: "#^Method Http\\\\Message\\\\MessageFactory\\\\DiactorosMessageFactory\\:\\:createRequest\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/MessageFactory/DiactorosMessageFactory.php + + - + message: "#^Method Http\\\\Message\\\\MessageFactory\\\\DiactorosMessageFactory\\:\\:createResponse\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/MessageFactory/DiactorosMessageFactory.php + + - + message: "#^Method Http\\\\Message\\\\MessageFactory\\\\GuzzleMessageFactory\\:\\:createRequest\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/MessageFactory/GuzzleMessageFactory.php + + - + message: "#^Method Http\\\\Message\\\\MessageFactory\\\\GuzzleMessageFactory\\:\\:createResponse\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/MessageFactory/GuzzleMessageFactory.php + + - + message: "#^Method Http\\\\Message\\\\MessageFactory\\\\SlimMessageFactory\\:\\:createRequest\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/MessageFactory/SlimMessageFactory.php + + - + message: "#^Method Http\\\\Message\\\\MessageFactory\\\\SlimMessageFactory\\:\\:createResponse\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/MessageFactory/SlimMessageFactory.php + + - + message: "#^Property Http\\\\Message\\\\RequestMatcher\\\\RequestMatcher\\:\\:\\$methods type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/RequestMatcher/RequestMatcher.php + + - + message: "#^Property Http\\\\Message\\\\RequestMatcher\\\\RequestMatcher\\:\\:\\$path \\(string\\) does not accept string\\|null\\.$#" + count: 1 + path: src/RequestMatcher/RequestMatcher.php + + - + message: "#^Property Http\\\\Message\\\\RequestMatcher\\\\RequestMatcher\\:\\:\\$host \\(string\\) does not accept string\\|null\\.$#" + count: 1 + path: src/RequestMatcher/RequestMatcher.php + + - + message: "#^Property Http\\\\Message\\\\Stream\\\\BufferedStream\\:\\:\\$resource \\(resource\\) does not accept resource\\|false\\.$#" + count: 2 + path: src/Stream/BufferedStream.php + + - + message: "#^Property Http\\\\Message\\\\Stream\\\\BufferedStream\\:\\:\\$stream \\(Psr\\\\Http\\\\Message\\\\StreamInterface\\) does not accept null\\.$#" + count: 1 + path: src/Stream/BufferedStream.php + + - + message: "#^Property Http\\\\Message\\\\Stream\\\\BufferedStream\\:\\:\\$resource \\(resource\\) does not accept null\\.$#" + count: 1 + path: src/Stream/BufferedStream.php + + - + message: "#^Parameter \\#1 \\$stream of class Slim\\\\Http\\\\Stream constructor expects resource, resource\\|false given\\.$#" + count: 1 + path: src/StreamFactory/SlimStreamFactory.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/UriFactory/SlimUriFactory.php + + - + message: "#^Call to function array_key_exists\\(\\) with 'chunk' and array will always evaluate to false\\.$#" + count: 1 + path: src/filters.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..6355eac --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,12 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: max + paths: + - src + excludePaths: + analyse: + - %currentWorkingDirectory%/src/MessageFactory/* + - %currentWorkingDirectory%/src/StreamFactory/* + - %currentWorkingDirectory%/src/UriFactory/* diff --git a/puli.json b/puli.json index 9237b10..024a85d 100644 --- a/puli.json +++ b/puli.json @@ -10,6 +10,16 @@ "depends": "Zend\\Diactoros\\Request" } }, + "0836751e-6558-4d1b-8993-4a52012947c3": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", + "type": "Http\\Message\\ResponseFactory" + }, + "1d127622-dc61-4bfa-b9da-d221548d72c3": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", + "type": "Http\\Message\\RequestFactory" + }, "2438c2d0-0658-441f-8855-ddaf0f87d54d": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory", @@ -50,6 +60,11 @@ "depends": "Zend\\Diactoros\\Uri" } }, + "4672a6ee-ad9e-4109-a5d1-b7d46f26c7a1": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", + "type": "Http\\Message\\MessageFactory" + }, "6234e947-d3bd-43eb-97d5-7f9e22e6bb1b": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory", @@ -58,6 +73,16 @@ "depends": "Zend\\Diactoros\\Response" } }, + "6a9ad6ce-d82c-470f-8e30-60f21d9d95bf": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\UriFactory\\SlimUriFactory", + "type": "Http\\Message\\UriFactory" + }, + "72c2afa0-ea56-4d03-adb6-a9f241a8a734": { + "_class": "Puli\\Discovery\\Binding\\ClassBinding", + "class": "Http\\Message\\StreamFactory\\SlimStreamFactory", + "type": "Http\\Message\\StreamFactory" + }, "95c1be8f-39fe-4abd-8351-92cb14379a75": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\StreamFactory\\DiactorosStreamFactory", diff --git a/spec/Authentication/AutoBasicAuthSpec.php b/spec/Authentication/AutoBasicAuthSpec.php index 33130ed..ba4ea8e 100644 --- a/spec/Authentication/AutoBasicAuthSpec.php +++ b/spec/Authentication/AutoBasicAuthSpec.php @@ -3,23 +3,22 @@ namespace spec\Http\Message\Authentication; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; class AutoBasicAuthSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Authentication\AutoBasicAuth'); } - function it_is_an_authentication() + public function it_is_an_authentication() { $this->shouldImplement('Http\Message\Authentication'); } - function it_authenticates_a_request( + public function it_authenticates_a_request( RequestInterface $request, UriInterface $uri, UriInterface $uriWithoutUserInfo, @@ -38,7 +37,7 @@ function it_authenticates_a_request( $this->authenticate($request)->shouldReturn($authenticatedRequest); } - function it_authenticates_a_request_without_password( + public function it_authenticates_a_request_without_password( RequestInterface $request, UriInterface $uri, UriInterface $uriWithoutUserInfo, @@ -57,7 +56,7 @@ function it_authenticates_a_request_without_password( $this->authenticate($request)->shouldReturn($authenticatedRequest); } - function it_does_not_authenticate_a_request(RequestInterface $request, UriInterface $uri) + public function it_does_not_authenticate_a_request(RequestInterface $request, UriInterface $uri) { $request->getUri()->willReturn($uri); $uri->getUserInfo()->willReturn(''); @@ -65,7 +64,7 @@ function it_does_not_authenticate_a_request(RequestInterface $request, UriInterf $this->authenticate($request)->shouldReturn($request); } - function it_authenticates_a_request_without_user_info_removal( + public function it_authenticates_a_request_without_user_info_removal( RequestInterface $request, UriInterface $uri, RequestInterface $authenticatedRequest diff --git a/spec/Authentication/BasicAuthSpec.php b/spec/Authentication/BasicAuthSpec.php index e70d097..c0e2b80 100644 --- a/spec/Authentication/BasicAuthSpec.php +++ b/spec/Authentication/BasicAuthSpec.php @@ -2,27 +2,27 @@ namespace spec\Http\Message\Authentication; -use Psr\Http\Message\RequestInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\RequestInterface; class BasicAuthSpec extends ObjectBehavior { - function let() + public function let() { $this->beConstructedWith('john.doe', 'secret'); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Authentication\BasicAuth'); } - function it_is_an_authentication() + public function it_is_an_authentication() { $this->shouldImplement('Http\Message\Authentication'); } - function it_authenticates_a_request(RequestInterface $request, RequestInterface $newRequest) + public function it_authenticates_a_request(RequestInterface $request, RequestInterface $newRequest) { $request->withHeader('Authorization', 'Basic '.base64_encode('john.doe:secret'))->willReturn($newRequest); diff --git a/spec/Authentication/BearerSpec.php b/spec/Authentication/BearerSpec.php index aaca72a..a8670ac 100644 --- a/spec/Authentication/BearerSpec.php +++ b/spec/Authentication/BearerSpec.php @@ -2,27 +2,27 @@ namespace spec\Http\Message\Authentication; -use Psr\Http\Message\RequestInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\RequestInterface; class BearerSpec extends ObjectBehavior { - function let() + public function let() { $this->beConstructedWith('token'); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Authentication\Bearer'); } - function it_is_an_authentication() + public function it_is_an_authentication() { $this->shouldImplement('Http\Message\Authentication'); } - function it_authenticates_a_request(RequestInterface $request, RequestInterface $newRequest) + public function it_authenticates_a_request(RequestInterface $request, RequestInterface $newRequest) { $request->withHeader('Authorization', 'Bearer token')->willReturn($newRequest); diff --git a/spec/Authentication/ChainSpec.php b/spec/Authentication/ChainSpec.php index 61e752b..55f5d9a 100644 --- a/spec/Authentication/ChainSpec.php +++ b/spec/Authentication/ChainSpec.php @@ -3,29 +3,29 @@ namespace spec\Http\Message\Authentication; use Http\Message\Authentication; -use Psr\Http\Message\RequestInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\RequestInterface; class ChainSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Authentication\Chain'); } - function it_is_an_authentication() + public function it_is_an_authentication() { $this->shouldImplement('Http\Message\Authentication'); } - function it_throws_an_exception_when_non_authentication_is_passed() + public function it_throws_an_exception_when_non_authentication_is_passed() { $this->beConstructedWith(['authentication']); $this->shouldThrow('InvalidArgumentException')->duringInstantiation(); } - function it_authenticates_a_request( + public function it_authenticates_a_request( Authentication $auth1, Authentication $auth2, RequestInterface $originalRequest, diff --git a/spec/Authentication/HeaderSpec.php b/spec/Authentication/HeaderSpec.php new file mode 100644 index 0000000..9558184 --- /dev/null +++ b/spec/Authentication/HeaderSpec.php @@ -0,0 +1,31 @@ +beConstructedWith('X-AUTH-TOKEN', 'REAL'); + } + + public function it_is_initializable() + { + $this->shouldHaveType('Http\Message\Authentication\Header'); + } + + public function it_is_an_authentication() + { + $this->shouldImplement('Http\Message\Authentication'); + } + + public function it_authenticates_a_request(RequestInterface $request, RequestInterface $newRequest) + { + $request->withHeader('X-AUTH-TOKEN', 'REAL')->willReturn($newRequest); + + $this->authenticate($request)->shouldReturn($newRequest); + } +} diff --git a/spec/Authentication/MatchingSpec.php b/spec/Authentication/MatchingSpec.php index 58f458b..23cd1d6 100644 --- a/spec/Authentication/MatchingSpec.php +++ b/spec/Authentication/MatchingSpec.php @@ -3,38 +3,38 @@ namespace spec\Http\Message\Authentication; use Http\Message\Authentication; -use Psr\Http\Message\RequestInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\RequestInterface; class MatchingSpec extends ObjectBehavior { - function let(Authentication $authentication) + public function let(Authentication $authentication) { - $matcher = function($request) { return true; }; + $matcher = function ($request) { return true; }; $this->beConstructedWith($authentication, $matcher); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Authentication\Matching'); } - function it_is_an_authentication() + public function it_is_an_authentication() { $this->shouldImplement('Http\Message\Authentication'); } - function it_authenticates_a_request(Authentication $authentication, RequestInterface $request, RequestInterface $newRequest) + public function it_authenticates_a_request(Authentication $authentication, RequestInterface $request, RequestInterface $newRequest) { $authentication->authenticate($request)->willReturn($newRequest); $this->authenticate($request)->shouldReturn($newRequest); } - function it_does_not_authenticate_a_request(Authentication $authentication, RequestInterface $request) + public function it_does_not_authenticate_a_request(Authentication $authentication, RequestInterface $request) { - $matcher = function($request) { return false; }; + $matcher = function ($request) { return false; }; $this->beConstructedWith($authentication, $matcher); @@ -43,7 +43,7 @@ function it_does_not_authenticate_a_request(Authentication $authentication, Requ $this->authenticate($request)->shouldReturn($request); } - function it_creates_a_matcher_from_url(Authentication $authentication) + public function it_creates_a_matcher_from_url(Authentication $authentication) { $this->createUrlMatcher($authentication, 'url')->shouldHaveType('Http\Message\Authentication\Matching'); } diff --git a/spec/Authentication/QueryParamSpec.php b/spec/Authentication/QueryParamSpec.php index 5e8b92e..eae560f 100644 --- a/spec/Authentication/QueryParamSpec.php +++ b/spec/Authentication/QueryParamSpec.php @@ -2,13 +2,13 @@ namespace spec\Http\Message\Authentication; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class QueryParamSpec extends ObjectBehavior { - function let() + public function let() { $this->beConstructedWith([ 'username' => 'username', @@ -16,17 +16,17 @@ function let() ]); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Authentication\QueryParam'); } - function it_is_an_authentication() + public function it_is_an_authentication() { $this->shouldImplement('Http\Message\Authentication'); } - function it_authenticates_a_request( + public function it_authenticates_a_request( RequestInterface $request, UriInterface $uri, RequestInterface $newRequest, diff --git a/spec/Authentication/RequestConditionalSpec.php b/spec/Authentication/RequestConditionalSpec.php index 5946736..f143f7a 100644 --- a/spec/Authentication/RequestConditionalSpec.php +++ b/spec/Authentication/RequestConditionalSpec.php @@ -4,27 +4,27 @@ use Http\Message\Authentication; use Http\Message\RequestMatcher; -use Psr\Http\Message\RequestInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\RequestInterface; class RequestConditionalSpec extends ObjectBehavior { - function let(RequestMatcher $requestMatcher, Authentication $authentication) + public function let(RequestMatcher $requestMatcher, Authentication $authentication) { $this->beConstructedWith($requestMatcher, $authentication); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Authentication\RequestConditional'); } - function it_is_an_authentication() + public function it_is_an_authentication() { $this->shouldImplement('Http\Message\Authentication'); } - function it_authenticates_a_request( + public function it_authenticates_a_request( Authentication $authentication, RequestMatcher $requestMatcher, RequestInterface $request, @@ -36,7 +36,7 @@ function it_authenticates_a_request( $this->authenticate($request)->shouldReturn($newRequest); } - function it_does_not_authenticate_a_request( + public function it_does_not_authenticate_a_request( Authentication $authentication, RequestMatcher $requestMatcher, RequestInterface $request diff --git a/spec/Authentication/WsseSpec.php b/spec/Authentication/WsseSpec.php index 926e9da..bb48c1f 100644 --- a/spec/Authentication/WsseSpec.php +++ b/spec/Authentication/WsseSpec.php @@ -2,34 +2,34 @@ namespace spec\Http\Message\Authentication; -use Psr\Http\Message\RequestInterface; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; class WsseSpec extends ObjectBehavior { - function let() + public function let() { $this->beConstructedWith('john.doe', 'secret'); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Authentication\Wsse'); } - function it_is_an_authentication() + public function it_is_an_authentication() { $this->shouldImplement('Http\Message\Authentication'); } - function it_authenticates_a_request( + public function it_authenticates_a_request( RequestInterface $request, RequestInterface $newRequest, RequestInterface $newerRequest ) { $request->withHeader('Authorization', 'WSSE profile="UsernameToken"')->willReturn($newRequest); - $newRequest->withHeader('X-WSSE', Argument::that(function($arg) { + $newRequest->withHeader('X-WSSE', Argument::that(function ($arg) { return preg_match('/UsernameToken Username=".*", PasswordDigest=".*", Nonce=".*", Created=".*"/', $arg); }))->willReturn($newerRequest); diff --git a/spec/Builder/ResponseBuilderSpec.php b/spec/Builder/ResponseBuilderSpec.php index 606884a..4dfe977 100644 --- a/spec/Builder/ResponseBuilderSpec.php +++ b/spec/Builder/ResponseBuilderSpec.php @@ -7,13 +7,13 @@ class ResponseBuilderSpec extends ObjectBehavior { - function it_is_initializable(ResponseInterface $response) + public function it_is_initializable(ResponseInterface $response) { $this->beConstructedWith($response); $this->shouldHaveType('Http\Message\Builder\ResponseBuilder'); } - function it_reads_headers_from_array(ResponseInterface $response) + public function it_reads_headers_from_array(ResponseInterface $response) { $response->withStatus(200, 'OK')->willReturn($response); $response->withProtocolVersion('1.1')->willReturn($response); @@ -23,13 +23,16 @@ function it_reads_headers_from_array(ResponseInterface $response) $this->setHeadersFromArray(['HTTP/1.1 200 OK', 'Content-type: text/html']); } - function it_reads_headers_from_string(ResponseInterface $response) + /** + * @see https://github.com/php-http/message/issues/41 + */ + public function it_splits_headers_correctly(ResponseInterface $response) { $response->withStatus(200, 'OK')->willReturn($response); $response->withProtocolVersion('1.1')->willReturn($response); $response->hasHeader('Content-type')->willReturn(false); - $response->withHeader('Content-type', 'text/html')->willReturn($response); + $response->withHeader('Content-type', 'application/xml+atom')->willReturn($response); $this->beConstructedWith($response); - $this->setHeadersFromString("HTTP/1.1 200 OK\r\nContent-type: text/html\r\n"); + $this->setHeadersFromString("HTTP/1.1 200 OK\r\nContent-type: application/xml+atom\r\n"); } } diff --git a/spec/CookieJarSpec.php b/spec/CookieJarSpec.php index 29afee4..9a8acb6 100644 --- a/spec/CookieJarSpec.php +++ b/spec/CookieJarSpec.php @@ -7,17 +7,17 @@ class CookieJarSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\CookieJar'); } - function it_is_an_iterator_aggregate() + public function it_is_an_iterator_aggregate() { $this->getIterator()->shouldHaveType('Iterator'); } - function it_has_a_cookie() + public function it_has_a_cookie() { $cookie = new Cookie('name', 'value'); @@ -27,7 +27,7 @@ function it_has_a_cookie() $this->hasCookies()->shouldReturn(true); } - function it_accepts_a_cookie() + public function it_accepts_a_cookie() { $cookie = new Cookie('name', 'value'); $cookie2 = new Cookie('name', 'value2'); @@ -39,7 +39,7 @@ function it_accepts_a_cookie() $this->hasCookie($cookie2)->shouldReturn(true); } - function it_removes_a_cookie_with_an_empty_value() + public function it_removes_a_cookie_with_an_empty_value() { $cookie = new Cookie('name', 'value'); $cookie2 = new Cookie('name'); @@ -51,7 +51,7 @@ function it_removes_a_cookie_with_an_empty_value() $this->hasCookie($cookie2)->shouldReturn(false); } - function it_removes_a_cookie_with_a_lower_expiration_time() + public function it_removes_a_cookie_with_a_lower_expiration_time() { $cookie = new Cookie('name', 'value', 100); $cookie2 = new Cookie('name', 'value', 1000); @@ -63,7 +63,7 @@ function it_removes_a_cookie_with_a_lower_expiration_time() $this->hasCookie($cookie2)->shouldReturn(true); } - function it_removes_a_cookie() + public function it_removes_a_cookie() { $cookie = new Cookie('name', 'value', 100); @@ -73,7 +73,7 @@ function it_removes_a_cookie() $this->hasCookie($cookie)->shouldReturn(false); } - function it_returns_all_cookies() + public function it_returns_all_cookies() { $cookie = new Cookie('name', 'value'); $cookie2 = new Cookie('name2', 'value'); @@ -84,7 +84,7 @@ function it_returns_all_cookies() $this->getCookies()->shouldBeAnArrayOfInstance('Http\Message\Cookie'); } - function it_returns_the_matching_cookies() + public function it_returns_the_matching_cookies() { $cookie = new Cookie('name', 'value'); $cookie2 = new Cookie('name', 'value2'); @@ -94,7 +94,7 @@ function it_returns_the_matching_cookies() $this->getMatchingCookies($cookie2)->shouldBeAnArrayOfInstance('Http\Message\Cookie'); } - function it_sets_cookies() + public function it_sets_cookies() { $cookie = new Cookie('name', 'value'); @@ -105,7 +105,7 @@ function it_sets_cookies() $this->count()->shouldReturn(1); } - function it_accepts_cookies() + public function it_accepts_cookies() { $cookie = new Cookie('name', 'value'); $cookie2 = new Cookie('name2', 'value'); @@ -119,7 +119,7 @@ function it_accepts_cookies() $this->count()->shouldReturn(2); } - function it_removes_cookies() + public function it_removes_cookies() { $cookie = new Cookie('name', 'value'); $cookie2 = new Cookie('name2', 'value'); @@ -133,7 +133,7 @@ function it_removes_cookies() $this->count()->shouldReturn(1); } - function it_removes_matching_cookies() + public function it_removes_matching_cookies() { $cookie = new Cookie('name', 'value'); $cookie2 = new Cookie('name2', 'value', 0, 'php-http.org'); @@ -148,7 +148,7 @@ function it_removes_matching_cookies() $this->count()->shouldReturn(1); } - function it_clears_cookies() + public function it_clears_cookies() { $cookie = new Cookie('name', 'value', 0, 'php-http.org'); $cookie2 = new Cookie('name2', 'value'); @@ -161,7 +161,7 @@ function it_clears_cookies() $this->count()->shouldReturn(0); } - public function getMatchers() + public function getMatchers(): array { return [ 'beAnArrayOfInstance' => function ($subject, $instance) { diff --git a/spec/CookieSpec.php b/spec/CookieSpec.php index 248968f..5f77e97 100644 --- a/spec/CookieSpec.php +++ b/spec/CookieSpec.php @@ -7,17 +7,17 @@ class CookieSpec extends ObjectBehavior { - function let() + public function let() { $this->beConstructedWith('name', 'value'); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Cookie'); } - function it_has_a_name() + public function it_has_a_name() { $this->getName()->shouldReturn('name'); } @@ -25,49 +25,50 @@ function it_has_a_name() /** * @dataProvider invalidCharacterExamples */ - function it_throws_an_exception_when_the_name_contains_invalid_character($name, $shouldThrow) + public function it_throws_an_exception_when_the_name_contains_invalid_character() { - $this->beConstructedWith($name); + foreach (self::invalidCharacterExamples() as $example) { + $this->beConstructedWith($example[0]); - if ($shouldThrow) { - $expectation = $this->shouldThrow('InvalidArgumentException'); - } else { - $expectation = $this->shouldNotThrow('InvalidArgumentException'); - } + if ($example[1]) { + $expectation = $this->shouldThrow('InvalidArgumentException'); + } else { + $expectation = $this->shouldNotThrow('InvalidArgumentException'); + } - $expectation->duringInstantiation(); + $expectation->duringInstantiation(); + } } - function it_throws_an_expection_when_name_is_empty() + public function it_throws_an_expection_when_name_is_empty() { $this->beConstructedWith(''); $this->shouldThrow('InvalidArgumentException')->duringInstantiation(); } - function it_has_a_value() + public function it_has_a_value() { $this->getValue()->shouldReturn('value'); $this->hasValue()->shouldReturn(true); } - /** - * @dataProvider invalidCharacterExamples - */ - function it_throws_an_exception_when_the_value_contains_invalid_character($value, $shouldThrow) + public function it_throws_an_exception_when_the_value_contains_invalid_character() { - $this->beConstructedWith('name', $value); + foreach (self::invalidCharacterExamples() as $example) { + $this->beConstructedWith('name', $example[0]); - if ($shouldThrow) { - $expectation = $this->shouldThrow('InvalidArgumentException'); - } else { - $expectation = $this->shouldNotThrow('InvalidArgumentException'); - } + if ($example[1]) { + $expectation = $this->shouldThrow('InvalidArgumentException'); + } else { + $expectation = $this->shouldNotThrow('InvalidArgumentException'); + } - $expectation->duringInstantiation(); + $expectation->duringInstantiation(); + } } - function it_accepts_a_value() + public function it_accepts_a_value() { $cookie = $this->withValue('value2'); @@ -75,21 +76,20 @@ function it_accepts_a_value() $cookie->getValue()->shouldReturn('value2'); } - /** - * @dataProvider invalidCharacterExamples - */ - function it_throws_an_exception_when_the_new_value_contains_invalid_character($value, $shouldThrow) + public function it_throws_an_exception_when_the_new_value_contains_invalid_character() { - if ($shouldThrow) { - $expectation = $this->shouldThrow('InvalidArgumentException'); - } else { - $expectation = $this->shouldNotThrow('InvalidArgumentException'); + foreach (self::invalidCharacterExamples() as $example) { + if ($example[1]) { + $expectation = $this->shouldThrow('InvalidArgumentException'); + } else { + $expectation = $this->shouldNotThrow('InvalidArgumentException'); + } + + $expectation->duringWithValue($example[0]); } - - $expectation->duringWithValue($value); } - function it_has_a_max_age_time() + public function it_has_a_max_age_time() { $this->beConstructedWith('name', 'value', 10); @@ -97,7 +97,7 @@ function it_has_a_max_age_time() $this->hasMaxAge()->shouldReturn(true); } - function it_accepts_a_max_age() + public function it_accepts_a_max_age() { $cookie = $this->withMaxAge(1); @@ -105,12 +105,12 @@ function it_accepts_a_max_age() $cookie->getMaxAge()->shouldReturn(1); } - function it_throws_an_exception_when_max_age_is_invalid() + public function it_throws_an_exception_when_max_age_is_invalid() { $this->shouldThrow('InvalidArgumentException')->duringWithMaxAge('-1'); } - function it_has_an_expires_attribute() + public function it_has_an_expires_attribute() { $expires = new \DateTime('+10 seconds'); @@ -121,7 +121,7 @@ function it_has_an_expires_attribute() $this->isExpired()->shouldReturn(false); } - function it_accepts_an_expires_attribute() + public function it_accepts_an_expires_attribute() { $expires = new \DateTime('+10 seconds'); @@ -131,7 +131,7 @@ function it_accepts_an_expires_attribute() $cookie->getExpires()->shouldReturn($expires); } - function it_is_expired() + public function it_is_expired() { $this->beConstructedWith('name', 'value', null, null, null, false, false, new \DateTime('-2 minutes')); @@ -140,13 +140,13 @@ function it_is_expired() $this->isExpired()->shouldReturn(true); } - function it_has_a_domain() + public function it_has_a_domain() { $this->getDomain()->shouldReturn(null); $this->hasDomain()->shouldReturn(false); } - function it_has_a_valid_domain() + public function it_has_a_valid_domain() { $this->beConstructedWith('name', 'value', null, '.PhP-hTtP.oRg'); @@ -154,7 +154,7 @@ function it_has_a_valid_domain() $this->hasDomain()->shouldReturn(true); } - function it_accepts_a_domain() + public function it_accepts_a_domain() { $cookie = $this->withDomain('.PhP-hTtP.oRg'); @@ -162,7 +162,7 @@ function it_accepts_a_domain() $cookie->getDomain()->shouldReturn('php-http.org'); } - function it_matches_a_domain() + public function it_matches_a_domain() { $this->beConstructedWith('name', 'value', null, 'php-http.org'); @@ -171,12 +171,12 @@ function it_matches_a_domain() $this->matchDomain('wWw.PhP-hTtP.oRg')->shouldReturn(true); } - function it_has_a_path() + public function it_has_a_path() { $this->getPath()->shouldReturn('/'); } - function it_accepts_a_path() + public function it_accepts_a_path() { $cookie = $this->withPath('/path'); @@ -184,27 +184,30 @@ function it_accepts_a_path() $cookie->getPath()->shouldReturn('/path'); } - function it_matches_a_path() + public function it_matches_a_path() { $this->beConstructedWith('name', 'value', null, null, '/path/to/somewhere'); $this->matchPath('/path/to/somewhere')->shouldReturn(true); + $this->matchPath('/path/to/somewhere/else')->shouldReturn(true); $this->matchPath('/path/to/somewhereelse')->shouldReturn(false); } - function it_matches_the_root_path() + public function it_matches_the_root_path() { $this->beConstructedWith('name', 'value', null, null, '/'); $this->matchPath('/')->shouldReturn(true); + $this->matchPath('/cookies')->shouldReturn(true); + $this->matchPath('/cookies/')->shouldReturn(true); } - function it_is_secure() + public function it_is_secure() { $this->isSecure()->shouldReturn(false); } - function it_accepts_security() + public function it_accepts_security() { $cookie = $this->withSecure(true); @@ -212,12 +215,12 @@ function it_accepts_security() $cookie->isSecure()->shouldReturn(true); } - function it_can_be_http_only() + public function it_can_be_http_only() { $this->isHttpOnly()->shouldReturn(false); } - function it_accepts_http_only() + public function it_accepts_http_only() { $cookie = $this->withHttpOnly(true); @@ -225,7 +228,7 @@ function it_accepts_http_only() $cookie->isHttpOnly()->shouldReturn(true); } - function it_matches_other_cookies() + public function it_matches_other_cookies() { $this->beConstructedWith('name', 'value', null, 'php-http.org'); @@ -236,12 +239,38 @@ function it_matches_other_cookies() $this->match($notMatches)->shouldReturn(false); } + public function it_validates_itself() + { + $this->isValid()->shouldReturn(true); + } + + public function it_can_be_constructed_without_name_validation() + { + $this->beConstructedThrough('createWithoutValidation', ["\x20"]); + + $this->isValid()->shouldReturn(false); + } + + public function it_can_be_constructed_without_value_validation() + { + $this->beConstructedThrough('createWithoutValidation', ['name', "\x20"]); + + $this->isValid()->shouldReturn(false); + } + + public function it_can_be_constructed_without_max_age_validation() + { + $this->beConstructedThrough('createWithoutValidation', ['name', 'value', '-1']); + + $this->isValid()->shouldReturn(false); + } + /** * Provides examples for invalid characers in names and values. * * @return array */ - public function invalidCharacterExamples() + private static function invalidCharacterExamples() { return [ ['a', false], @@ -249,7 +278,7 @@ public function invalidCharacterExamples() ['z', false], ["\x20", true], ['0', false], - ["\x7F", true] + ["\x7F", true], ]; } } diff --git a/spec/CookieUtilSpec.php b/spec/CookieUtilSpec.php new file mode 100644 index 0000000..3777b0a --- /dev/null +++ b/spec/CookieUtilSpec.php @@ -0,0 +1,156 @@ +testCookieParse('Friday, 31 Jul 20 08:49:37 GMT', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_2() + { + $this->testCookieParse('Friday, 31-Jul-20 08:49:37 GMT', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_3() + { + $this->testCookieParse('Fri, 31-Jul-2020 08:49:37 GMT', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_4() + { + $this->testCookieParse('Fri, 31 Jul 2020 08:49:37 GMT', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_5() + { + $this->testCookieParse('Fri, 31-07-2020 08:49:37 GMT', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_6() + { + $this->testCookieParse('Fri, 31-07-20 08:49:37 GMT', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_7() + { + $this->testCookieParse('Friday, 31-Jul-20 08:49:37 GMT', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_8() + { + $this->testCookieParse('Fri Jul 31 08:49:37 2020', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_9() + { + $this->testCookieParse('Friday July 31st 2020, 08:49:37 GMT', 'Friday, 31-Jul-2020 08:49:37 +0000'); + } + + public function it_parses_cookie_date_string_10() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('Wed, 09 Jun 2021 10:18:14 GMT', 'Wednesday, 09-Jun-2021 10:18:14 +0000'); + } + + public function it_parses_cookie_date_string_11() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('Wed, 09 Jun 2021 22:18:14 GMT', 'Wednesday, 09-Jun-2021 22:18:14 +0000'); + } + + public function it_parses_cookie_date_string_12() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('Tue, 18 Oct 2011 07:42:42.123 GMT', 'Tuesday, 18-Oct-2011 07:42:42 +0000'); + } + + public function it_parses_cookie_date_string_13() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('18 Oct 2011 07:42:42 GMT', 'Tuesday, 18-Oct-2011 07:42:42 +0000'); + } + + public function it_parses_cookie_date_string_14() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('8 Oct 2011 7:42:42 GMT', 'Saturday, 08-Oct-2011 07:42:42 +0000'); + } + + public function it_parses_cookie_date_string_15() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('8 Oct 2011 7:2:42 GMT', 'Saturday, 08-Oct-2011 07:02:42 +0000'); + } + + public function it_parses_cookie_date_string_16() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('Oct 18 2011 07:42:42 GMT', 'Tuesday, 18-Oct-2011 07:42:42 +0000'); + } + + public function it_parses_cookie_date_string_17() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('Tue Oct 18 2011 07:05:03 GMT+0000 (GMT)', 'Tuesday, 18-Oct-2011 07:05:03 +0000'); + } + + public function it_parses_cookie_date_string_18() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('09 Jun 2021 10:18:14 GMT', 'Wednesday, 09-Jun-2021 10:18:14 +0000'); + } + + public function it_parses_cookie_date_string_19() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('01 Jan 1970 00:00:00 GMT', 'Thursday, 01-Jan-1970 00:00:00 +0000'); + } + + public function it_parses_cookie_date_string_20() + { + // https://github.com/salesforce/tough-cookie/blob/master/test/date_test.js#L52 + $this->testCookieParse('01 Jan 1601 00:00:00 GMT', 'Monday, 01-Jan-1601 00:00:00 +0000'); + } + + public function it_parses_cookie_date_string_21() + { + // implicit year + $this->testCookieParse('10 Feb 81 13:00:00 GMT', 'Tuesday, 10-Feb-1981 13:00:00 +0000'); + } + + public function it_parses_cookie_date_string_22() + { + // dashes + $this->testCookieParse('Thu, 17-Apr-2014 02:12:29 GMT', 'Thursday, 17-Apr-2014 02:12:29 +0000'); + } + + public function it_parses_cookie_date_string_23() + { + // dashes and UTC + $this->testCookieParse('Thu, 17-Apr-2014 02:12:29 UTC', 'Thursday, 17-Apr-2014 02:12:29 +0000'); + } + + public function it_throws_an_exception_if_cookie_date_string_is_unparseable_1() + { + $this->beConstructedThrough('parseDate', ['Flursday July 31st 2020, 08:49:37 GMT']); + $this->shouldThrow('Http\Message\Exception\UnexpectedValueException'); + } + + public function it_throws_an_exception_if_cookie_date_string_is_unparseable_2() + { + $this->beConstructedThrough('parseDate', ['99 Jix 3038 48:86:72 ZMT']); + $this->shouldThrow('Http\Message\Exception\UnexpectedValueException'); + } + + private function testCookieParse(string $input, string $output) + { + $this->beConstructedThrough('parseDate', [$input]); + $this->shouldHaveType('\DateTime'); + $this->format('l, d-M-Y H:i:s O')->shouldReturn($output); + } +} diff --git a/spec/Decorator/MessageDecoratorSpec.php b/spec/Decorator/MessageDecoratorSpec.php index 53f79ec..8c8bd33 100644 --- a/spec/Decorator/MessageDecoratorSpec.php +++ b/spec/Decorator/MessageDecoratorSpec.php @@ -3,46 +3,45 @@ namespace spec\Http\Message\Decorator; use Http\Message\Decorator\MessageDecorator; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\StreamInterface; -use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class MessageDecoratorSpec extends ObjectBehavior { - function let(MessageInterface $message) + public function let(MessageInterface $message) { $this->beAnInstanceOf('spec\Http\Message\Decorator\MessageDecoratorStub', [$message]); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('spec\Http\Message\Decorator\MessageDecoratorStub'); } - function it_is_a_message() + public function it_is_a_message() { $this->shouldImplement('Psr\Http\Message\MessageInterface'); } - function it_is_a_message_decorator() + public function it_is_a_message_decorator() { $this->shouldUseTrait('Http\Message\Decorator\MessageDecorator'); } - function it_has_a_message() + public function it_has_a_message() { $this->getMessage()->shouldImplement('Psr\Http\Message\MessageInterface'); } - function it_has_a_protocol_version(MessageInterface $message) + public function it_has_a_protocol_version(MessageInterface $message) { $message->getProtocolVersion()->willReturn('1.1'); $this->getProtocolVersion()->shouldReturn('1.1'); } - function it_accepts_a_protocol_version(MessageInterface $message, MessageInterface $newMessage) + public function it_accepts_a_protocol_version(MessageInterface $message, MessageInterface $newMessage) { $message->withProtocolVersion('1.1')->willReturn($newMessage); @@ -50,10 +49,10 @@ function it_accepts_a_protocol_version(MessageInterface $message, MessageInterfa $new->getMessage()->shouldReturn($newMessage); } - function it_has_headers(MessageInterface $message) + public function it_has_headers(MessageInterface $message) { $headers = [ - 'Content-Type' => 'application/xml' + 'Content-Type' => 'application/xml', ]; $message->getHeaders()->willReturn($headers); @@ -61,28 +60,28 @@ function it_has_headers(MessageInterface $message) $this->getHeaders()->shouldReturn($headers); } - function it_can_check_a_header(MessageInterface $message) + public function it_can_check_a_header(MessageInterface $message) { $message->hasHeader('Content-Type')->willReturn(true); $this->hasHeader('Content-Type')->shouldReturn(true); } - function it_has_a_header(MessageInterface $message) + public function it_has_a_header(MessageInterface $message) { - $message->getHeader('Content-Type')->willReturn('application/xml'); + $message->getHeader('Content-Type')->willReturn(['application/xml']); - $this->getHeader('Content-Type')->shouldReturn('application/xml'); + $this->getHeader('Content-Type')->shouldReturn(['application/xml']); } - function it_has_a_header_line(MessageInterface $message) + public function it_has_a_header_line(MessageInterface $message) { $message->getHeaderLine('Accept-Encoding')->willReturn('gzip, deflate'); $this->getHeaderLine('Accept-Encoding')->shouldReturn('gzip, deflate'); } - function it_accepts_a_header(MessageInterface $message, MessageInterface $newMessage) + public function it_accepts_a_header(MessageInterface $message, MessageInterface $newMessage) { $message->withHeader('Content-Type', 'application/xml')->willReturn($newMessage); @@ -90,7 +89,7 @@ function it_accepts_a_header(MessageInterface $message, MessageInterface $newMes $new->getMessage()->shouldReturn($newMessage); } - function it_accepts_added_headers(MessageInterface $message, MessageInterface $newMessage) + public function it_accepts_added_headers(MessageInterface $message, MessageInterface $newMessage) { $message->withAddedHeader('Content-Type', 'application/xml')->willReturn($newMessage); @@ -98,7 +97,7 @@ function it_accepts_added_headers(MessageInterface $message, MessageInterface $n $new->getMessage()->shouldReturn($newMessage); } - function it_removes_a_header(MessageInterface $message, MessageInterface $newMessage) + public function it_removes_a_header(MessageInterface $message, MessageInterface $newMessage) { $message->withoutHeader('Content-Type')->willReturn($newMessage); @@ -106,14 +105,14 @@ function it_removes_a_header(MessageInterface $message, MessageInterface $newMes $new->getMessage()->shouldReturn($newMessage); } - function it_has_a_body(MessageInterface $message, StreamInterface $body) + public function it_has_a_body(MessageInterface $message, StreamInterface $body) { $message->getBody()->willReturn($body); $this->getBody()->shouldReturn($body); } - function it_accepts_a_body(MessageInterface $message, MessageInterface $newMessage, StreamInterface $body) + public function it_accepts_a_body(MessageInterface $message, MessageInterface $newMessage, StreamInterface $body) { $message->withBody($body)->willReturn($newMessage); @@ -121,12 +120,12 @@ function it_accepts_a_body(MessageInterface $message, MessageInterface $newMessa $new->getMessage()->shouldReturn($newMessage); } - function getMatchers() + public function getMatchers(): array { return [ 'useTrait' => function ($subject, $trait) { return class_uses($subject, $trait); - } + }, ]; } } diff --git a/spec/Decorator/RequestDecoratorSpec.php b/spec/Decorator/RequestDecoratorSpec.php index 776cb94..b1e200c 100644 --- a/spec/Decorator/RequestDecoratorSpec.php +++ b/spec/Decorator/RequestDecoratorSpec.php @@ -3,53 +3,52 @@ namespace spec\Http\Message\Decorator; use Http\Message\Decorator\RequestDecorator; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class RequestDecoratorSpec extends ObjectBehavior { - function let(RequestInterface $request) + public function let(RequestInterface $request) { $this->beAnInstanceOf('spec\Http\Message\Decorator\RequestDecoratorStub', [$request]); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('spec\Http\Message\Decorator\RequestDecoratorStub'); } - function it_is_a_request() + public function it_is_a_request() { $this->shouldImplement('Psr\Http\Message\RequestInterface'); } - function it_is_a_request_decorator() + public function it_is_a_request_decorator() { $this->shouldUseTrait('Http\Message\Decorator\RequestDecorator'); } - function it_has_a_request() + public function it_has_a_request() { $this->getRequest()->shouldImplement('Psr\Http\Message\RequestInterface'); } - function it_accepts_a_request(RequestInterface $request) + public function it_accepts_a_request(RequestInterface $request) { $new = $this->withRequest($request); $new->getRequest()->shouldReturn($request); } - function it_has_a_request_target(RequestInterface $request) + public function it_has_a_request_target(RequestInterface $request) { $request->getRequestTarget()->willReturn('/'); $this->getRequestTarget()->shouldReturn('/'); } - function it_accepts_a_request_target(RequestInterface $request, RequestInterface $newRequest) + public function it_accepts_a_request_target(RequestInterface $request, RequestInterface $newRequest) { $request->withRequestTarget('/')->willReturn($newRequest); @@ -57,14 +56,14 @@ function it_accepts_a_request_target(RequestInterface $request, RequestInterface $new->getMessage()->shouldReturn($newRequest); } - function it_has_a_method(RequestInterface $request) + public function it_has_a_method(RequestInterface $request) { $request->getMethod()->willReturn('GET'); $this->getMethod()->shouldReturn('GET'); } - function it_accepts_a_method(RequestInterface $request, RequestInterface $newRequest) + public function it_accepts_a_method(RequestInterface $request, RequestInterface $newRequest) { $request->withMethod('GET')->willReturn($newRequest); @@ -72,14 +71,14 @@ function it_accepts_a_method(RequestInterface $request, RequestInterface $newReq $new->getMessage()->shouldReturn($newRequest); } - function it_has_an_uri(RequestInterface $request, UriInterface $uri) + public function it_has_an_uri(RequestInterface $request, UriInterface $uri) { $request->getUri()->willReturn($uri); $this->getUri()->shouldReturn($uri); } - function it_accepts_an_uri(RequestInterface $request, RequestInterface $newRequest, UriInterface $uri) + public function it_accepts_an_uri(RequestInterface $request, RequestInterface $newRequest, UriInterface $uri) { $request->withUri($uri, false)->willReturn($newRequest); @@ -87,12 +86,12 @@ function it_accepts_an_uri(RequestInterface $request, RequestInterface $newReque $new->getMessage()->shouldReturn($newRequest); } - function getMatchers() + public function getMatchers(): array { return [ 'useTrait' => function ($subject, $trait) { return class_uses($subject, $trait); - } + }, ]; } } diff --git a/spec/Decorator/ResponseDecoratorSpec.php b/spec/Decorator/ResponseDecoratorSpec.php index 7fdd61c..3c7798b 100644 --- a/spec/Decorator/ResponseDecoratorSpec.php +++ b/spec/Decorator/ResponseDecoratorSpec.php @@ -3,52 +3,51 @@ namespace spec\Http\Message\Decorator; use Http\Message\Decorator\ResponseDecorator; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\ResponseInterface; class ResponseDecoratorSpec extends ObjectBehavior { - function let(ResponseInterface $response) + public function let(ResponseInterface $response) { $this->beAnInstanceOf('spec\Http\Message\Decorator\ResponseDecoratorStub', [$response]); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('spec\Http\Message\Decorator\ResponseDecoratorStub'); } - function it_is_a_response() + public function it_is_a_response() { $this->shouldImplement('Psr\Http\Message\ResponseInterface'); } - function it_is_a_response_decorator() + public function it_is_a_response_decorator() { $this->shouldUseTrait('Http\Message\Decorator\ResponseDecorator'); } - function it_has_a_response() + public function it_has_a_response() { $this->getResponse()->shouldImplement('Psr\Http\Message\ResponseInterface'); } - function it_accepts_a_response(ResponseInterface $response) + public function it_accepts_a_response(ResponseInterface $response) { $new = $this->withResponse($response); $new->getResponse()->shouldReturn($response); } - function it_has_a_status_code(ResponseInterface $response) + public function it_has_a_status_code(ResponseInterface $response) { $response->getStatusCode()->willReturn(200); $this->getStatusCode()->shouldReturn(200); } - function it_accepts_a_status(ResponseInterface $response, ResponseInterface $newResponse) + public function it_accepts_a_status(ResponseInterface $response, ResponseInterface $newResponse) { $response->withStatus(200, 'OK')->willReturn($newResponse); @@ -56,19 +55,19 @@ function it_accepts_a_status(ResponseInterface $response, ResponseInterface $new $new->getMessage()->shouldReturn($newResponse); } - function it_has_a_reason_phrase(ResponseInterface $response) + public function it_has_a_reason_phrase(ResponseInterface $response) { $response->getReasonPhrase()->willReturn('OK'); $this->getReasonPhrase()->shouldReturn('OK'); } - function getMatchers() + public function getMatchers(): array { return [ 'useTrait' => function ($subject, $trait) { return class_uses($subject, $trait); - } + }, ]; } } diff --git a/spec/Decorator/StreamDecoratorSpec.php b/spec/Decorator/StreamDecoratorSpec.php index 055e385..1315579 100644 --- a/spec/Decorator/StreamDecoratorSpec.php +++ b/spec/Decorator/StreamDecoratorSpec.php @@ -3,126 +3,125 @@ namespace spec\Http\Message\Decorator; use Http\Message\Decorator\StreamDecorator; -use Psr\Http\Message\StreamInterface; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; +use Psr\Http\Message\StreamInterface; class StreamDecoratorSpec extends ObjectBehavior { - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { $this->beAnInstanceOf('spec\Http\Message\Decorator\StreamDecoratorStub', [$stream]); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('spec\Http\Message\Decorator\StreamDecoratorStub'); } - function it_is_a_stream_decorator() + public function it_is_a_stream_decorator() { $this->shouldUseTrait('Http\Message\Decorator\StreamDecorator'); } - function it_casts_the_stream_to_string(StreamInterface $stream) + public function it_casts_the_stream_to_string(StreamInterface $stream) { $stream->__toString()->willReturn('body'); $this->__toString()->shouldReturn('body'); } - function it_closes_the_stream(StreamInterface $stream) + public function it_closes_the_stream(StreamInterface $stream) { $stream->close()->shouldBeCalled(); $this->close(); } - function it_detaches_the_stream(StreamInterface $stream) + public function it_detaches_the_stream(StreamInterface $stream) { $stream->detach()->willReturn('detached'); $this->detach()->shouldReturn('detached'); } - function it_returns_the_size_of_the_stream(StreamInterface $stream) + public function it_returns_the_size_of_the_stream(StreamInterface $stream) { $stream->getSize()->willReturn(1234); $this->getSize()->shouldReturn(1234); } - function it_returns_the_current_position_of_the_stream(StreamInterface $stream) + public function it_returns_the_current_position_of_the_stream(StreamInterface $stream) { $stream->tell()->willReturn(0); $this->tell()->shouldReturn(0); } - function it_checks_whether_the_stream_is_eof(StreamInterface $stream) + public function it_checks_whether_the_stream_is_eof(StreamInterface $stream) { $stream->eof()->willReturn(false); $this->eof()->shouldReturn(false); } - function it_checks_whether_the_stream_is_seekable(StreamInterface $stream) + public function it_checks_whether_the_stream_is_seekable(StreamInterface $stream) { $stream->isSeekable()->willReturn(true); $this->isSeekable()->shouldReturn(true); } - function it_seeks_the_current_position_of_the_stream(StreamInterface $stream) + public function it_seeks_the_current_position_of_the_stream(StreamInterface $stream) { $stream->seek(0, SEEK_SET)->shouldBeCalled(); $this->seek(0); } - function it_rewinds_to_the_beginning_of_the_stream(StreamInterface $stream) + public function it_rewinds_to_the_beginning_of_the_stream(StreamInterface $stream) { $stream->rewind()->shouldBeCalled(); $this->rewind(); } - function it_checks_whether_the_stream_is_writable(StreamInterface $stream) + public function it_checks_whether_the_stream_is_writable(StreamInterface $stream) { $stream->isWritable()->willReturn(true); $this->isWritable()->shouldReturn(true); } - function it_writes_to_the_stream(StreamInterface $stream) + public function it_writes_to_the_stream(StreamInterface $stream) { - $stream->write('body')->shouldBeCalled(); + $stream->write('body')->shouldBeCalled()->willReturn(4); $this->write('body'); } - function it_checks_whether_the_stream_is_readable(StreamInterface $stream) + public function it_checks_whether_the_stream_is_readable(StreamInterface $stream) { $stream->isReadable()->willReturn(true); $this->isReadable()->shouldReturn(true); } - function it_reads_from_the_stream(StreamInterface $stream) + public function it_reads_from_the_stream(StreamInterface $stream) { $stream->read(4)->willReturn('body'); $this->read(4)->shouldReturn('body'); } - function it_returns_the_contents_of_the_stream(StreamInterface $stream) + public function it_returns_the_contents_of_the_stream(StreamInterface $stream) { $stream->getContents()->willReturn('body'); $this->getContents()->shouldReturn('body'); } - function it_returns_metadata_of_the_stream(StreamInterface $stream) + public function it_returns_metadata_of_the_stream(StreamInterface $stream) { $stream->getMetadata(null)->willReturn(['key' => 'value']); $stream->getMetadata('key')->willReturn('value'); @@ -133,12 +132,12 @@ function it_returns_metadata_of_the_stream(StreamInterface $stream) $this->getMetadata('key2')->shouldReturn(null); } - function getMatchers() + public function getMatchers(): array { return [ 'useTrait' => function ($subject, $trait) { return class_uses($subject, $trait); - } + }, ]; } } diff --git a/spec/Encoding/ChunkStreamSpec.php b/spec/Encoding/ChunkStreamSpec.php index eff8a41..b008112 100644 --- a/spec/Encoding/ChunkStreamSpec.php +++ b/spec/Encoding/ChunkStreamSpec.php @@ -2,29 +2,24 @@ namespace spec\Http\Message\Encoding; -use Psr\Http\Message\StreamInterface; -use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\StreamInterface; class ChunkStreamSpec extends ObjectBehavior { use StreamBehavior; - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { - if (defined('HHVM_VERSION')) { - throw new SkippingException('Skipping test as there is no dechunk filter on hhvm'); - } - $this->beConstructedWith($stream); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Encoding\ChunkStream'); } - function it_chunks_content() + public function it_chunks_content() { $stream = new MemoryStream('This is a stream'); $this->beConstructedWith($stream, 6); @@ -32,11 +27,19 @@ function it_chunks_content() $this->getContents()->shouldReturn("10\r\nThis is a stream\r\n0\r\n\r\n"); } - function it_chunks_in_multiple() + public function it_chunks_in_multiple() { $stream = new MemoryStream('This is a stream', 6); $this->beConstructedWith($stream, 6); $this->getContents()->shouldReturn("6\r\nThis i\r\n6\r\ns a st\r\n4\r\nream\r\n0\r\n\r\n"); } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream('This is a stream'); + $this->beConstructedWith($stream, 6); + + $this->getSize()->shouldReturn(null); + } } diff --git a/spec/Encoding/CompressStreamSpec.php b/spec/Encoding/CompressStreamSpec.php index 4e96849..79ac873 100644 --- a/spec/Encoding/CompressStreamSpec.php +++ b/spec/Encoding/CompressStreamSpec.php @@ -2,29 +2,25 @@ namespace spec\Http\Message\Encoding; -use Psr\Http\Message\StreamInterface; -use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\StreamInterface; class CompressStreamSpec extends ObjectBehavior { - use StreamBehavior, ZlibStreamBehavior; + use StreamBehavior; + use ZlibStreamBehavior; - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { - if (defined('HHVM_VERSION')) { - throw new SkippingException('Skipping test as zlib is not working on hhvm'); - } - $this->beConstructedWith($stream); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Encoding\CompressStream'); } - function it_reads() + public function it_reads() { $stream = new MemoryStream('This is a test stream'); $this->beConstructedWith($stream); @@ -33,7 +29,7 @@ function it_reads() $this->read(4)->shouldReturn(substr(gzcompress('This is a test stream'), 0, 4)); } - function it_gets_content() + public function it_gets_content() { $stream = new MemoryStream('This is a test stream'); $this->beConstructedWith($stream); @@ -41,4 +37,13 @@ function it_gets_content() $stream->rewind(); $this->getContents()->shouldReturn(gzcompress('This is a test stream')); } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream('This is a test stream'); + $this->beConstructedWith($stream); + + $stream->rewind(); + $this->getSize()->shouldReturn(null); + } } diff --git a/spec/Encoding/DechunkStreamSpec.php b/spec/Encoding/DechunkStreamSpec.php index 715e33a..5a57fe0 100644 --- a/spec/Encoding/DechunkStreamSpec.php +++ b/spec/Encoding/DechunkStreamSpec.php @@ -2,29 +2,24 @@ namespace spec\Http\Message\Encoding; -use Psr\Http\Message\StreamInterface; -use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\StreamInterface; class DechunkStreamSpec extends ObjectBehavior { use StreamBehavior; - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { - if (defined('HHVM_VERSION')) { - throw new SkippingException('Skipping test as there is no dechunk filter on hhvm'); - } - $this->beConstructedWith($stream); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Encoding\DechunkStream'); } - function it_reads() + public function it_reads() { $stream = new MemoryStream("4\r\ntest\r\n4\r\ntest\r\n0\r\n\r\n\0"); $this->beConstructedWith($stream); @@ -33,11 +28,19 @@ function it_reads() $this->read(6)->shouldReturn('st'); } - function it_gets_content() + public function it_gets_content() { $stream = new MemoryStream("4\r\ntest\r\n0\r\n\r\n\0"); $this->beConstructedWith($stream); $this->getContents()->shouldReturn('test'); } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream("4\r\ntest\r\n4\r\ntest\r\n0\r\n\r\n\0"); + $this->beConstructedWith($stream); + + $this->getSize()->shouldReturn(null); + } } diff --git a/spec/Encoding/DecompressStreamSpec.php b/spec/Encoding/DecompressStreamSpec.php index d3295ff..cee6c9a 100644 --- a/spec/Encoding/DecompressStreamSpec.php +++ b/spec/Encoding/DecompressStreamSpec.php @@ -2,29 +2,25 @@ namespace spec\Http\Message\Encoding; -use Psr\Http\Message\StreamInterface; -use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\StreamInterface; class DecompressStreamSpec extends ObjectBehavior { - use StreamBehavior, ZlibStreamBehavior; + use StreamBehavior; + use ZlibStreamBehavior; - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { - if (defined('HHVM_VERSION')) { - throw new SkippingException('Skipping test as zlib is not working on hhvm'); - } - $this->beConstructedWith($stream); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Encoding\DecompressStream'); } - function it_reads() + public function it_reads() { // "This is a test stream" | deflate $stream = new MemoryStream(gzcompress('This is a test stream')); @@ -33,7 +29,7 @@ function it_reads() $this->read(4)->shouldReturn('This'); } - function it_gets_content() + public function it_gets_content() { // "This is a test stream" | deflate $stream = new MemoryStream(gzcompress('This is a test stream')); @@ -41,4 +37,12 @@ function it_gets_content() $this->getContents()->shouldReturn('This is a test stream'); } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream(gzcompress('This is a test stream')); + $this->beConstructedWith($stream); + + $this->getSize()->shouldReturn(null); + } } diff --git a/spec/Encoding/DeflateStreamSpec.php b/spec/Encoding/DeflateStreamSpec.php index 89c9406..008c58d 100644 --- a/spec/Encoding/DeflateStreamSpec.php +++ b/spec/Encoding/DeflateStreamSpec.php @@ -2,33 +2,33 @@ namespace spec\Http\Message\Encoding; -use Psr\Http\Message\StreamInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\StreamInterface; class DeflateStreamSpec extends ObjectBehavior { use StreamBehavior; - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { $this->beConstructedWith($stream); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Encoding\DeflateStream'); } - function it_reads() + public function it_reads() { $stream = new MemoryStream('This is a test stream'); $this->beConstructedWith($stream); $stream->rewind(); - $this->read(4)->shouldReturn(substr(gzdeflate('This is a test stream'),0, 4)); + $this->read(4)->shouldReturn(substr(gzdeflate('This is a test stream'), 0, 4)); } - function it_gets_content() + public function it_gets_content() { $stream = new MemoryStream('This is a test stream'); $this->beConstructedWith($stream); @@ -36,4 +36,12 @@ function it_gets_content() $stream->rewind(); $this->getContents()->shouldReturn(gzdeflate('This is a test stream')); } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream('This stream is a test stream'); + $this->beConstructedWith($stream); + + $this->getSize()->shouldReturn(null); + } } diff --git a/spec/Encoding/FilteredStreamStubSpec.php b/spec/Encoding/FilteredStreamStubSpec.php new file mode 100644 index 0000000..fbaf68c --- /dev/null +++ b/spec/Encoding/FilteredStreamStubSpec.php @@ -0,0 +1,69 @@ +beAnInstanceOf('spec\Http\Message\Encoding\FilteredStreamStub'); + $this->beConstructedWith($stream, 'foo'); + $this->shouldThrow('RuntimeException')->duringInstantiation(); + } + + public function it_throws_during_instantiation_with_invalid_write_filter_options(StreamInterface $stream) + { + $this->beAnInstanceOf('spec\Http\Message\Encoding\FilteredStreamStub'); + $this->beConstructedWith($stream, null, 'foo'); + $this->shouldThrow('RuntimeException')->duringInstantiation(); + } + + public function let(StreamInterface $stream) + { + $this->beAnInstanceOf('spec\Http\Message\Encoding\FilteredStreamStub'); + $this->beConstructedWith($stream); + } + + public function it_reads() + { + // "This is a test stream" | base64_encode + $stream = new MemoryStream('This is a test stream'); + $this->beConstructedWith($stream); + + $this->read(4)->shouldReturn('VGhp'); + } + + public function it_gets_content() + { + // "This is a test stream" | base64_encode + $stream = new MemoryStream('This is a test stream'); + $this->beConstructedWith($stream); + + $this->getContents()->shouldReturn('VGhpcyBpcyBhIHRlc3Qgc3RyZWFt'); + } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream(gzencode('This is a test stream')); + $this->beConstructedWith($stream); + + $this->getSize()->shouldBeNull(); + } +} + +class FilteredStreamStub extends FilteredStream +{ + protected function readFilter(): string + { + return 'convert.base64-encode'; + } + + protected function writeFilter(): string + { + return 'convert.base64-encode'; + } +} diff --git a/spec/Encoding/GzipDecodeStreamSpec.php b/spec/Encoding/GzipDecodeStreamSpec.php index 5243238..32ea393 100644 --- a/spec/Encoding/GzipDecodeStreamSpec.php +++ b/spec/Encoding/GzipDecodeStreamSpec.php @@ -2,29 +2,25 @@ namespace spec\Http\Message\Encoding; -use Psr\Http\Message\StreamInterface; -use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\StreamInterface; class GzipDecodeStreamSpec extends ObjectBehavior { - use StreamBehavior, ZlibStreamBehavior; + use StreamBehavior; + use ZlibStreamBehavior; - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { - if (defined('HHVM_VERSION')) { - throw new SkippingException('Skipping test as zlib is not working on hhvm'); - } - $this->beConstructedWith($stream); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Encoding\GzipDecodeStream'); } - function it_reads() + public function it_reads() { // "This is a test stream" | deflate $stream = new MemoryStream(gzencode('This is a test stream')); @@ -33,7 +29,7 @@ function it_reads() $this->read(4)->shouldReturn('This'); } - function it_gets_content() + public function it_gets_content() { // "This is a test stream" | deflate $stream = new MemoryStream(gzencode('This is a test stream')); @@ -41,4 +37,12 @@ function it_gets_content() $this->getContents()->shouldReturn('This is a test stream'); } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream(gzencode('This is a test stream')); + $this->beConstructedWith($stream); + + $this->getSize()->shouldReturn(null); + } } diff --git a/spec/Encoding/GzipEncodeStreamSpec.php b/spec/Encoding/GzipEncodeStreamSpec.php index b11d4c5..bcce227 100644 --- a/spec/Encoding/GzipEncodeStreamSpec.php +++ b/spec/Encoding/GzipEncodeStreamSpec.php @@ -2,38 +2,34 @@ namespace spec\Http\Message\Encoding; -use Psr\Http\Message\StreamInterface; -use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\StreamInterface; class GzipEncodeStreamSpec extends ObjectBehavior { - use StreamBehavior, ZlibStreamBehavior; + use StreamBehavior; + use ZlibStreamBehavior; - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { - if (defined('HHVM_VERSION')) { - throw new SkippingException('Skipping test as zlib is not working on hhvm'); - } - $this->beConstructedWith($stream); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Encoding\GzipEncodeStream'); } - function it_reads() + public function it_reads() { $stream = new MemoryStream('This is a test stream'); $this->beConstructedWith($stream); $stream->rewind(); - $this->read(4)->shouldReturn(substr(gzencode('This is a test stream'),0, 4)); + $this->read(4)->shouldReturn(substr(gzencode('This is a test stream'), 0, 4)); } - function it_gets_content() + public function it_gets_content() { $stream = new MemoryStream('This is a test stream'); $this->beConstructedWith($stream); @@ -41,4 +37,13 @@ function it_gets_content() $stream->rewind(); $this->getContents()->shouldReturn(gzencode('This is a test stream')); } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream('This is a test stream'); + $this->beConstructedWith($stream); + + $stream->rewind(); + $this->getSize()->shouldReturn(null); + } } diff --git a/spec/Encoding/InflateStreamSpec.php b/spec/Encoding/InflateStreamSpec.php index 6dcd846..f61ff84 100644 --- a/spec/Encoding/InflateStreamSpec.php +++ b/spec/Encoding/InflateStreamSpec.php @@ -2,24 +2,25 @@ namespace spec\Http\Message\Encoding; -use Psr\Http\Message\StreamInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\StreamInterface; class InflateStreamSpec extends ObjectBehavior { - use StreamBehavior, ZlibStreamBehavior; + use StreamBehavior; + use ZlibStreamBehavior; - function let(StreamInterface $stream) + public function let(StreamInterface $stream) { $this->beConstructedWith($stream); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Encoding\InflateStream'); } - function it_reads() + public function it_reads() { // "This is a test stream" | deflate $stream = new MemoryStream(gzdeflate('This is a test stream')); @@ -28,7 +29,7 @@ function it_reads() $this->read(4)->shouldReturn('This'); } - function it_gets_content() + public function it_gets_content() { // "This is a test stream" | deflate $stream = new MemoryStream(gzdeflate('This is a test stream')); @@ -36,4 +37,12 @@ function it_gets_content() $this->getContents()->shouldReturn('This is a test stream'); } + + public function it_does_not_know_the_content_size() + { + $stream = new MemoryStream(gzdeflate('This stream is a test stream')); + $this->beConstructedWith($stream); + + $this->getSize()->shouldReturn(null); + } } diff --git a/spec/Encoding/MemoryStream.php b/spec/Encoding/MemoryStream.php index 4403536..bd61cc3 100644 --- a/spec/Encoding/MemoryStream.php +++ b/spec/Encoding/MemoryStream.php @@ -10,7 +10,9 @@ class MemoryStream implements StreamInterface private $size = 0; - public function __construct($body = "", $chunkSize = null) + private $chunkSize; + + public function __construct($body = '', $chunkSize = 8192) { $this->size = strlen($body); $this->resource = fopen('php://memory', 'rw+'); @@ -22,14 +24,16 @@ public function __construct($body = "", $chunkSize = null) fwrite($this->resource, $body); fseek($this->resource, 0); + + $this->chunkSize = $chunkSize; } - public function __toString() + public function __toString(): string { return $this->getContents(); } - public function close() + public function close(): void { fclose($this->resource); } @@ -42,64 +46,64 @@ public function detach() return $resource; } - public function getSize() + public function getSize(): int { return $this->size; } - public function tell() + public function tell(): int { return ftell($this->resource); } - public function eof() + public function eof(): bool { return feof($this->resource); } - public function isSeekable() + public function isSeekable(): bool { return true; } - public function seek($offset, $whence = SEEK_SET) + public function seek(int $offset, int $whence = SEEK_SET): void { fseek($this->resource, $offset, $whence); } - public function rewind() + public function rewind(): void { $this->seek(0); } - public function isWritable() + public function isWritable(): bool { return true; } - public function write($string) + public function write(string $string): int { - fwrite($this->resource, $string); + return fwrite($this->resource, $string); } - public function isReadable() + public function isReadable(): bool { return true; } - public function read($length) + public function read(int $length): string { - return fread($this->resource, $length); + return fread($this->resource, min($this->chunkSize, $length)); } - public function getContents() + public function getContents(): string { $this->rewind(); return $this->read($this->size); } - public function getMetadata($key = null) + public function getMetadata(?string $key = null) { $metadata = stream_get_meta_data($this->resource); diff --git a/spec/Encoding/StreamBehavior.php b/spec/Encoding/StreamBehavior.php index 54e1f5e..5834a03 100644 --- a/spec/Encoding/StreamBehavior.php +++ b/spec/Encoding/StreamBehavior.php @@ -4,7 +4,7 @@ trait StreamBehavior { - function it_is_a_stream() + public function it_is_a_stream() { $this->shouldImplement('Psr\Http\Message\StreamInterface'); } diff --git a/spec/Encoding/ZlibStreamBehavior.php b/spec/Encoding/ZlibStreamBehavior.php index bcd8311..39dce20 100644 --- a/spec/Encoding/ZlibStreamBehavior.php +++ b/spec/Encoding/ZlibStreamBehavior.php @@ -1,13 +1,9 @@ shouldHaveType('Http\Message\Formatter\CurlCommandFormatter'); + } + + public function it_is_a_formatter() + { + $this->shouldImplement('Http\Message\Formatter'); + } + + public function it_formats_the_request(RequestInterface $request, UriInterface $uri, StreamInterface $body) + { + $request->getUri()->willReturn($uri); + $request->getBody()->willReturn($body); + + $uri->withFragment('')->shouldBeCalled()->willReturn($uri); + $uri->__toString()->shouldBeCalled()->willReturn('http://foo.com/bar'); + $request->getMethod()->willReturn('GET'); + $request->getProtocolVersion()->willReturn('1.1'); + + $request->getHeaders()->willReturn(['foo' => ['bar', 'baz']]); + $request->getHeaderLine('foo')->willReturn('bar, baz'); + + $this->formatRequest($request)->shouldReturn('curl \'http://foo.com/bar\' -H \'foo: bar, baz\''); + } + + public function it_formats_post_request(RequestInterface $request, UriInterface $uri, StreamInterface $body) + { + $request->getUri()->willReturn($uri); + $request->getBody()->willReturn($body); + + $body->__toString()->willReturn('body " data'." test' bar"); + $body->getSize()->willReturn(1); + $body->isSeekable()->willReturn(true); + $body->rewind()->shouldBeCalled(); + + $uri->withFragment('')->shouldBeCalled()->willReturn($uri); + $uri->__toString()->shouldBeCalled()->willReturn('http://foo.com/bar'); + $request->getMethod()->willReturn('POST'); + $request->getProtocolVersion()->willReturn('2.0'); + + $request->getHeaders()->willReturn([]); + + $this->formatRequest($request)->shouldReturn("curl 'http://foo.com/bar' --http2 --request POST --data 'body \" data test'\'' bar'"); + } + + public function it_does_nothing_for_response(ResponseInterface $response, RequestInterface $request) + { + $this->formatResponse($response)->shouldReturn(''); + $this->formatResponseForRequest($response, $request)->shouldReturn(''); + } + + public function it_formats_the_request_with_user_agent(RequestInterface $request, UriInterface $uri, StreamInterface $body) + { + $request->getUri()->willReturn($uri); + $request->getBody()->willReturn($body); + + $uri->withFragment('')->shouldBeCalled()->willReturn($uri); + $uri->__toString()->shouldBeCalled()->willReturn('http://foo.com/bar'); + $request->getMethod()->willReturn('GET'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn(['user-agent' => ['foobar-browser']]); + + $this->formatRequest($request)->shouldReturn("curl 'http://foo.com/bar' -A 'foobar-browser'"); + } + + public function it_formats_requests_with_null_bytes(RequestInterface $request, UriInterface $uri, StreamInterface $body) + { + $request->getUri()->willReturn($uri); + $request->getBody()->willReturn($body); + + $body->__toString()->willReturn("\0"); + $body->getSize()->willReturn(1); + $body->isSeekable()->willReturn(true); + $body->rewind()->shouldBeCalled(); + + $uri->withFragment('')->shouldBeCalled()->willReturn($uri); + $uri->__toString()->shouldBeCalled()->willReturn('http://foo.com/bar'); + $request->getMethod()->willReturn('POST'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([]); + + $this->formatRequest($request)->shouldReturn("curl 'http://foo.com/bar' --request POST --data '[binary stream omitted]'"); + } + + public function it_formats_requests_with_line_break(RequestInterface $request, UriInterface $uri, StreamInterface $body) + { + $request->getUri()->willReturn($uri); + $request->getBody()->willReturn($body); + + $body->__toString()->willReturn("foo\nbar"); + $body->getSize()->willReturn(1); + $body->isSeekable()->willReturn(true); + $body->rewind()->shouldBeCalled(); + + $uri->withFragment('')->shouldBeCalled()->willReturn($uri); + $uri->__toString()->shouldBeCalled()->willReturn('http://foo.com/bar'); + $request->getMethod()->willReturn('POST'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([]); + + $this->formatRequest($request)->shouldReturn("curl 'http://foo.com/bar' --request POST --data 'foo\nbar'"); + } + + public function it_formats_requests_with_nonseekable_body(RequestInterface $request, UriInterface $uri, StreamInterface $body) + { + $request->getUri()->willReturn($uri); + $request->getBody()->willReturn($body); + + $body->getSize()->willReturn(1); + $body->isSeekable()->willReturn(false); + $body->__toString()->shouldNotBeCalled(); + $body->rewind()->shouldNotBeCalled(); + + $uri->withFragment('')->shouldBeCalled()->willReturn($uri); + $uri->__toString()->shouldBeCalled()->willReturn('http://foo.com/bar'); + $request->getMethod()->willReturn('POST'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([]); + + $this->formatRequest($request)->shouldReturn("curl 'http://foo.com/bar' --request POST --data '[non-seekable stream omitted]'"); + } + + public function it_formats_requests_with_long_body(RequestInterface $request, UriInterface $uri, StreamInterface $body) + { + $request->getUri()->willReturn($uri); + $request->getBody()->willReturn($body); + + $body->__toString()->shouldNotBeCalled(); + $body->getSize()->willReturn(2097153); + $body->isSeekable()->willReturn(true); + $body->rewind()->shouldNotBeCalled(); + + $uri->withFragment('')->shouldBeCalled()->willReturn($uri); + $uri->__toString()->shouldBeCalled()->willReturn('http://foo.com/bar'); + $request->getMethod()->willReturn('POST'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([]); + + $this->formatRequest($request)->shouldReturn("curl 'http://foo.com/bar' --request POST --data '[too long stream omitted]'"); + } +} diff --git a/spec/Formatter/FullHttpMessageFormatterSpec.php b/spec/Formatter/FullHttpMessageFormatterSpec.php new file mode 100644 index 0000000..e5fa362 --- /dev/null +++ b/spec/Formatter/FullHttpMessageFormatterSpec.php @@ -0,0 +1,295 @@ +beConstructedWith($maxBodyLength); + } + + public function it_is_initializable() + { + $this->shouldHaveType('Http\Message\Formatter\FullHttpMessageFormatter'); + } + + public function it_is_a_formatter() + { + $this->shouldImplement('Http\Message\Formatter'); + } + + public function it_formats_the_request_with_size_limit(RequestInterface $request, StreamInterface $stream) + { + $this->beConstructedWith(18); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $stream->__toString()->willReturn('This is an HTML stream request content.'); + $request->getBody()->willReturn($stream); + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/foo'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([ + 'X-Param-Foo' => ['foo'], + 'X-Param-Bar' => ['bar'], + ]); + + $expectedMessage = <<formatRequest($request)->shouldReturn($expectedMessage); + } + + public function it_formats_the_request_without_size_limit(RequestInterface $request, StreamInterface $stream) + { + $this->beConstructedWith(null); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $stream->__toString()->willReturn('This is an HTML stream request content.'); + $request->getBody()->willReturn($stream); + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/foo'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([ + 'X-Param-Foo' => ['foo'], + 'X-Param-Bar' => ['bar'], + ]); + + $expectedMessage = <<formatRequest($request)->shouldReturn($expectedMessage); + } + + public function it_does_not_format_the_request(RequestInterface $request, StreamInterface $stream) + { + $this->beConstructedWith(0); + + $stream->isSeekable()->willReturn(true); + $stream->__toString()->willReturn('This is an HTML stream request content.'); + $request->getBody()->willReturn($stream); + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/foo'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([ + 'X-Param-Foo' => ['foo'], + 'X-Param-Bar' => ['bar'], + ]); + + $expectedMessage = <<formatRequest($request)->shouldReturn($expectedMessage); + } + + public function it_does_not_format_no_seekable_request(RequestInterface $request, StreamInterface $stream) + { + $this->beConstructedWith(1000); + + $stream->isSeekable()->willReturn(false); + $stream->__toString()->willReturn('This is an HTML stream request content.'); + $request->getBody()->willReturn($stream); + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/foo'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([ + 'X-Param-Foo' => ['foo'], + 'X-Param-Bar' => ['bar'], + ]); + + $expectedMessage = <<formatRequest($request)->shouldReturn($expectedMessage); + } + + public function it_formats_the_response_with_size_limit(ResponseInterface $response, StreamInterface $stream, RequestInterface $request) + { + $this->beConstructedWith(18); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $stream->__toString()->willReturn('This is an HTML stream response content.'); + $response->getBody()->willReturn($stream); + $response->getProtocolVersion()->willReturn('1.1'); + $response->getStatusCode()->willReturn(200); + $response->getReasonPhrase()->willReturn('OK'); + $response->getHeaders()->willReturn([ + 'X-Param-Foo' => ['foo'], + 'X-Param-Bar' => ['bar'], + ]); + + $expectedMessage = <<formatResponse($response)->shouldReturn($expectedMessage); + $this->formatResponseForRequest($response, $request)->shouldReturn($expectedMessage); + } + + public function it_formats_the_response_without_size_limit(ResponseInterface $response, StreamInterface $stream) + { + $this->beConstructedWith(null); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $stream->__toString()->willReturn('This is an HTML stream response content.'); + $response->getBody()->willReturn($stream); + $response->getProtocolVersion()->willReturn('1.1'); + $response->getStatusCode()->willReturn(200); + $response->getReasonPhrase()->willReturn('OK'); + $response->getHeaders()->willReturn([ + 'X-Param-Foo' => ['foo'], + 'X-Param-Bar' => ['bar'], + ]); + + $expectedMessage = <<formatResponse($response)->shouldReturn($expectedMessage); + } + + public function it_does_not_format_the_response(ResponseInterface $response, StreamInterface $stream) + { + $this->beConstructedWith(0); + + $stream->isSeekable()->willReturn(true); + $stream->__toString()->willReturn('This is an HTML stream response content.'); + $response->getBody()->willReturn($stream); + $response->getProtocolVersion()->willReturn('1.1'); + $response->getStatusCode()->willReturn(200); + $response->getReasonPhrase()->willReturn('OK'); + $response->getHeaders()->willReturn([ + 'X-Param-Foo' => ['foo'], + 'X-Param-Bar' => ['bar'], + ]); + + $expectedMessage = <<formatResponse($response)->shouldReturn($expectedMessage); + } + + public function it_does_not_format_no_seekable_response(ResponseInterface $response, StreamInterface $stream) + { + $this->beConstructedWith(1000); + + $stream->isSeekable()->willReturn(false); + $stream->__toString()->willReturn('This is an HTML stream response content.'); + $response->getBody()->willReturn($stream); + $response->getProtocolVersion()->willReturn('1.1'); + $response->getStatusCode()->willReturn(200); + $response->getReasonPhrase()->willReturn('OK'); + $response->getHeaders()->willReturn([ + 'X-Param-Foo' => ['foo'], + 'X-Param-Bar' => ['bar'], + ]); + + $expectedMessage = <<formatResponse($response)->shouldReturn($expectedMessage); + } + + public function it_omits_body_with_null_bytes(RequestInterface $request, StreamInterface $stream) + { + $this->beConstructedWith(1); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $stream->__toString()->willReturn("\0"); + $request->getBody()->willReturn($stream); + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/foo'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([]); + + $expectedMessage = <<formatRequest($request)->shouldReturn($expectedMessage); + } + + public function it_allows_to_change_binary_detection(RequestInterface $request, StreamInterface $stream) + { + $this->beConstructedWith(1, '/\x01/'); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $stream->__toString()->willReturn("\0"); + $request->getBody()->willReturn($stream); + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/foo'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([]); + + $expectedMessage = <<formatRequest($request)->shouldReturn($expectedMessage); + } + + public function it_omits_body_with_line_break(RequestInterface $request, StreamInterface $stream) + { + $this->beConstructedWith(7); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $stream->__toString()->willReturn("foo\nbar"); + $request->getBody()->willReturn($stream); + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/foo'); + $request->getProtocolVersion()->willReturn('1.1'); + $request->getHeaders()->willReturn([]); + + $expectedMessage = <<formatRequest($request)->shouldReturn($expectedMessage); + } +} diff --git a/spec/Formatter/SimpleFormatterSpec.php b/spec/Formatter/SimpleFormatterSpec.php index ef70eb8..c8fc5cf 100644 --- a/spec/Formatter/SimpleFormatterSpec.php +++ b/spec/Formatter/SimpleFormatterSpec.php @@ -2,24 +2,24 @@ namespace spec\Http\Message\Formatter; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class SimpleFormatterSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\Formatter\SimpleFormatter'); } - function it_is_a_formatter() + public function it_is_a_formatter() { $this->shouldImplement('Http\Message\Formatter'); } - function it_formats_the_request(RequestInterface $request, UriInterface $uri) + public function it_formats_the_request(RequestInterface $request, UriInterface $uri) { $uri->__toString()->willReturn('http://foo.com/bar'); $request->getMethod()->willReturn('GET'); @@ -29,12 +29,13 @@ function it_formats_the_request(RequestInterface $request, UriInterface $uri) $this->formatRequest($request)->shouldReturn('GET http://foo.com/bar 1.1'); } - function it_formats_the_response(ResponseInterface $response) + public function it_formats_the_response(ResponseInterface $response, RequestInterface $request) { $response->getReasonPhrase()->willReturn('OK'); $response->getProtocolVersion()->willReturn('1.1'); $response->getStatusCode()->willReturn('200'); $this->formatResponse($response)->shouldReturn('200 OK 1.1'); + $this->formatResponseForRequest($response, $request)->shouldReturn('200 OK 1.1'); } } diff --git a/spec/MessageFactory/DiactorosMessageFactorySpec.php b/spec/MessageFactory/DiactorosMessageFactorySpec.php index 8dea0ad..89cffb3 100644 --- a/spec/MessageFactory/DiactorosMessageFactorySpec.php +++ b/spec/MessageFactory/DiactorosMessageFactorySpec.php @@ -8,7 +8,7 @@ class DiactorosMessageFactorySpec extends ObjectBehavior { use MessageFactoryBehavior; - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\MessageFactory\DiactorosMessageFactory'); } diff --git a/spec/MessageFactory/GuzzleMessageFactorySpec.php b/spec/MessageFactory/GuzzleMessageFactorySpec.php index f885555..be7de66 100644 --- a/spec/MessageFactory/GuzzleMessageFactorySpec.php +++ b/spec/MessageFactory/GuzzleMessageFactorySpec.php @@ -8,7 +8,7 @@ class GuzzleMessageFactorySpec extends ObjectBehavior { use MessageFactoryBehavior; - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\MessageFactory\GuzzleMessageFactory'); } diff --git a/spec/MessageFactory/MessageFactoryBehavior.php b/spec/MessageFactory/MessageFactoryBehavior.php index 9472ed4..42c51ae 100644 --- a/spec/MessageFactory/MessageFactoryBehavior.php +++ b/spec/MessageFactory/MessageFactoryBehavior.php @@ -4,21 +4,23 @@ trait MessageFactoryBehavior { - function it_is_a_message_factory() + public function it_is_a_message_factory() { $this->shouldImplement('Http\Message\MessageFactory'); } - function it_creates_a_request() + public function it_creates_a_request() { - $request = $this->createRequest('GET', '/'); + $request = $this->createRequest('GET', '/', ['X-hello' => 'world'], 'lorem'); $request->shouldHaveType('Psr\Http\Message\RequestInterface'); $request->getMethod()->shouldReturn('GET'); $request->getRequestTarget()->shouldReturn('/'); + $request->getBody()->__toString()->shouldReturn('lorem'); + $request->getHeaderLine('X-hello')->shouldReturn('world'); } - function it_creates_a_response() + public function it_creates_a_response() { $response = $this->createResponse(); diff --git a/spec/MessageFactory/SlimMessageFactorySpec.php b/spec/MessageFactory/SlimMessageFactorySpec.php new file mode 100644 index 0000000..b549a55 --- /dev/null +++ b/spec/MessageFactory/SlimMessageFactorySpec.php @@ -0,0 +1,18 @@ +shouldHaveType('Http\Message\MessageFactory\SlimMessageFactory'); + } +} diff --git a/spec/RequestMatcher/CallbackRequestMatcherSpec.php b/spec/RequestMatcher/CallbackRequestMatcherSpec.php index 6aa27df..fbf12a5 100644 --- a/spec/RequestMatcher/CallbackRequestMatcherSpec.php +++ b/spec/RequestMatcher/CallbackRequestMatcherSpec.php @@ -3,27 +3,26 @@ namespace spec\Http\Message\RequestMatcher; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; use Psr\Http\Message\RequestInterface; class CallbackRequestMatcherSpec extends ObjectBehavior { - function let() + public function let() { $this->beConstructedWith(function () {}); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\RequestMatcher\CallbackRequestMatcher'); } - function it_is_a_request_matcher() + public function it_is_a_request_matcher() { $this->shouldImplement('Http\Message\RequestMatcher'); } - function it_matches_a_request(RequestInterface $request) + public function it_matches_a_request(RequestInterface $request) { $callback = function () { return true; }; @@ -32,7 +31,7 @@ function it_matches_a_request(RequestInterface $request) $this->matches($request)->shouldReturn(true); } - function it_does_not_match_a_request(RequestInterface $request) + public function it_does_not_match_a_request(RequestInterface $request) { $callback = function () { return false; }; diff --git a/spec/RequestMatcher/RegexRequestMatcherSpec.php b/spec/RequestMatcher/RegexRequestMatcherSpec.php index e6e2c28..9f1dc7a 100644 --- a/spec/RequestMatcher/RegexRequestMatcherSpec.php +++ b/spec/RequestMatcher/RegexRequestMatcherSpec.php @@ -2,29 +2,28 @@ namespace spec\Http\Message\RequestMatcher; -use Http\Message\RequestMatcher; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class RegexRequestMatcherSpec extends ObjectBehavior { - function let($regex) + public function let($regex) { $this->beConstructedWith($regex); } - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\RequestMatcher\RegexRequestMatcher'); } - function it_is_a_request_matcher() + public function it_is_a_request_matcher() { $this->shouldImplement('Http\Message\RequestMatcher'); } - function it_matches_a_request(RequestInterface $request, UriInterface $uri) + public function it_matches_a_request(RequestInterface $request, UriInterface $uri) { $this->beConstructedWith('/test/'); @@ -34,7 +33,7 @@ function it_matches_a_request(RequestInterface $request, UriInterface $uri) $this->matches($request)->shouldReturn(true); } - function it_does_not_match_a_request(RequestInterface $request, UriInterface $uri) + public function it_does_not_match_a_request(RequestInterface $request, UriInterface $uri) { $this->beConstructedWith('/test/'); diff --git a/spec/RequestMatcher/RequestMatcherSpec.php b/spec/RequestMatcher/RequestMatcherSpec.php index 6ea1f92..0a000f1 100644 --- a/spec/RequestMatcher/RequestMatcherSpec.php +++ b/spec/RequestMatcher/RequestMatcherSpec.php @@ -2,24 +2,23 @@ namespace spec\Http\Message\RequestMatcher; -use Http\Message\RequestMatcher; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class RequestMatcherSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\RequestMatcher\RequestMatcher'); } - function it_is_a_request_matcher() + public function it_is_a_request_matcher() { $this->shouldImplement('Http\Message\RequestMatcher'); } - function it_matches_a_path(RequestInterface $request, UriInterface $uri) + public function it_matches_a_path(RequestInterface $request, UriInterface $uri) { $this->beConstructedWith('^/tes?'); @@ -29,7 +28,7 @@ function it_matches_a_path(RequestInterface $request, UriInterface $uri) $this->matches($request)->shouldReturn(true); } - function it_does_not_match_a_path(RequestInterface $request, UriInterface $uri) + public function it_does_not_match_a_path(RequestInterface $request, UriInterface $uri) { $this->beConstructedWith('#^/tes?#'); @@ -39,8 +38,7 @@ function it_does_not_match_a_path(RequestInterface $request, UriInterface $uri) $this->matches($request)->shouldReturn(false); } - - function it_matches_a_host(RequestInterface $request, UriInterface $uri) + public function it_matches_a_host(RequestInterface $request, UriInterface $uri) { $this->beConstructedWith(null, 'php-htt?'); @@ -50,7 +48,7 @@ function it_matches_a_host(RequestInterface $request, UriInterface $uri) $this->matches($request)->shouldReturn(true); } - function it_does_not_match_a_host(RequestInterface $request, UriInterface $uri) + public function it_does_not_match_a_host(RequestInterface $request, UriInterface $uri) { $this->beConstructedWith(null, 'php-htt?'); @@ -60,7 +58,7 @@ function it_does_not_match_a_host(RequestInterface $request, UriInterface $uri) $this->matches($request)->shouldReturn(false); } - function it_matches_a_method(RequestInterface $request) + public function it_matches_a_method(RequestInterface $request) { $this->beConstructedWith(null, null, 'get'); @@ -69,7 +67,7 @@ function it_matches_a_method(RequestInterface $request) $this->matches($request)->shouldReturn(true); } - function it_does_not_match_a_method(RequestInterface $request) + public function it_does_not_match_a_method(RequestInterface $request) { $this->beConstructedWith(null, null, 'get'); @@ -78,8 +76,7 @@ function it_does_not_match_a_method(RequestInterface $request) $this->matches($request)->shouldReturn(false); } - - function it_matches_a_scheme(RequestInterface $request, UriInterface $uri) + public function it_matches_a_scheme(RequestInterface $request, UriInterface $uri) { $this->beConstructedWith(null, null, null, 'http'); @@ -89,7 +86,7 @@ function it_matches_a_scheme(RequestInterface $request, UriInterface $uri) $this->matches($request)->shouldReturn(true); } - function it_does_not_match_a_scheme(RequestInterface $request, UriInterface $uri) + public function it_does_not_match_a_scheme(RequestInterface $request, UriInterface $uri) { $this->beConstructedWith(null, null, null, 'http'); diff --git a/spec/Stream/BufferedStreamSpec.php b/spec/Stream/BufferedStreamSpec.php new file mode 100644 index 0000000..f80fa69 --- /dev/null +++ b/spec/Stream/BufferedStreamSpec.php @@ -0,0 +1,184 @@ +beConstructedWith($stream); + + $stream->getSize()->willReturn(null); + } + + public function it_is_castable_to_string(StreamInterface $stream) + { + $eofCounter = 0; + $stream->eof()->will(function () use (&$eofCounter) { + return ++$eofCounter > 1; + }); + + $stream->read(8192)->willReturn('foo'); + + $this->__toString()->shouldReturn('foo'); + } + + public function it_detachs(StreamInterface $stream) + { + $stream->eof()->willReturn(true); + $stream->read(8192)->willReturn(''); + $stream->close()->shouldBeCalled(); + + $this->detach()->shouldBeResource(); + $this->detach()->shouldBeNull(); + } + + public function it_gets_size(StreamInterface $stream) + { + $stream->eof()->willReturn(false); + $this->getSize()->shouldReturn(null); + + $eofCounter = 0; + $stream->eof()->will(function () use (&$eofCounter) { + return ++$eofCounter > 1; + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->getSize()->shouldReturn(3); + } + + public function it_tells(StreamInterface $stream) + { + $this->tell()->shouldReturn(0); + + $eofCounter = 0; + $stream->eof()->will(function () use (&$eofCounter) { + return ++$eofCounter > 1; + }); + + $stream->read(8192)->willReturn('foo'); + $this->getContents()->shouldReturn('foo'); + $this->tell()->shouldReturn(3); + } + + public function it_eof(StreamInterface $stream) + { + // Case when underlying is false + $stream->eof()->willReturn(false); + $this->eof()->shouldReturn(false); + + // Case when sync and underlying is true + $stream->eof()->willReturn(true); + $this->eof()->shouldReturn(true); + + // Case not sync but underlying is true + $eofCounter = 0; + $stream->eof()->will(function () use (&$eofCounter) { + return ++$eofCounter > 1; + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->seek(0); + + $stream->eof()->willReturn(true); + $this->eof()->shouldReturn(false); + } + + public function it_is_seekable(StreamInterface $stream) + { + $this->isSeekable()->shouldReturn(true); + } + + public function it_seeks(StreamInterface $stream) + { + $this->seek(0); + $this->tell()->shouldReturn(0); + + $eofCounter = 0; + $stream->eof()->will(function () use (&$eofCounter) { + return ++$eofCounter > 1; + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->seek(2); + $this->tell()->shouldReturn(2); + } + + public function it_rewinds(StreamInterface $stream) + { + $this->rewind(); + $this->tell()->shouldReturn(0); + + $eofCounter = 0; + $stream->eof()->will(function () use (&$eofCounter) { + return ++$eofCounter > 1; + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->tell()->shouldReturn(3); + $this->rewind(); + $this->tell()->shouldReturn(0); + } + + public function it_is_not_writable(StreamInterface $stream) + { + $this->isWritable()->shouldReturn(false); + $this->shouldThrow('\RuntimeException')->duringWrite('foo'); + } + + public function it_is_readable(StreamInterface $stream) + { + $this->isReadable()->shouldReturn(true); + } + + public function it_reads(StreamInterface $stream) + { + $eofCounter = 0; + $stream->eof()->will(function () use (&$eofCounter) { + return ++$eofCounter > 1; + }); + + $stream->read(3)->willReturn('foo'); + $this->read(3)->shouldReturn('foo'); + + $stream->read(3)->willReturn('bar'); + $this->read(3)->shouldReturn('bar'); + + $this->rewind(); + $this->read(4)->shouldReturn('foob'); + + $stream->read(3)->willReturn('baz'); + $this->read(5)->shouldReturn('arbaz'); + } + + public function it_get_contents(StreamInterface $stream) + { + $eofCounter = 0; + $stream->eof()->will(function () use (&$eofCounter) { + return ++$eofCounter > 1; + }); + + $stream->read(8192)->willReturn('foo'); + + $this->getContents()->shouldReturn('foo'); + $this->eof()->shouldReturn(true); + } + + public function it_get_metadatas(StreamInterface $stream) + { + $this->getMetadata()->shouldBeArray(); + $this->getMetadata('unexistant')->shouldBeNull(); + $this->getMetadata('stream_type')->shouldReturn('TEMP'); + } +} diff --git a/spec/StreamFactory/DiactorosStreamFactorySpec.php b/spec/StreamFactory/DiactorosStreamFactorySpec.php index 372ef1b..f7da767 100644 --- a/spec/StreamFactory/DiactorosStreamFactorySpec.php +++ b/spec/StreamFactory/DiactorosStreamFactorySpec.php @@ -2,19 +2,19 @@ namespace spec\Http\Message\StreamFactory; -use Zend\Diactoros\Stream; +use Laminas\Diactoros\Stream; use PhpSpec\ObjectBehavior; class DiactorosStreamFactorySpec extends ObjectBehavior { use StreamFactoryBehavior; - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\StreamFactory\DiactorosStreamFactory'); } - function it_creates_a_stream_from_stream() + public function it_creates_a_stream_from_stream() { $this->createStream(new Stream('php://memory')) ->shouldHaveType('Psr\Http\Message\StreamInterface'); diff --git a/spec/StreamFactory/SlimStreamFactorySpec.php b/spec/StreamFactory/SlimStreamFactorySpec.php new file mode 100644 index 0000000..308f739 --- /dev/null +++ b/spec/StreamFactory/SlimStreamFactorySpec.php @@ -0,0 +1,26 @@ +shouldHaveType('Http\Message\StreamFactory\SlimStreamFactory'); + } + + public function it_creates_a_stream_from_stream() + { + $resource = fopen('php://memory', 'rw'); + $this->createStream(new Stream($resource)) + ->shouldHaveType('Psr\Http\Message\StreamInterface'); + } +} diff --git a/spec/StreamFactory/StreamFactoryBehavior.php b/spec/StreamFactory/StreamFactoryBehavior.php index 88ab4f4..5a4953c 100644 --- a/spec/StreamFactory/StreamFactoryBehavior.php +++ b/spec/StreamFactory/StreamFactoryBehavior.php @@ -2,26 +2,66 @@ namespace spec\Http\Message\StreamFactory; +use GuzzleHttp\Psr7\Stream; +use Psr\Http\Message\StreamInterface; + trait StreamFactoryBehavior { - function it_is_a_stream_factory() + public function it_is_a_stream_factory() { $this->shouldImplement('Http\Message\StreamFactory'); } - function it_creates_a_stream_from_string() + public function it_creates_a_stream_from_string() { $this->createStream('foo')->shouldHaveType('Psr\Http\Message\StreamInterface'); } - function it_creates_a_stream_from_resource() + public function it_creates_a_stream_from_resource() { $this->createStream(fopen('php://memory', 'rw')) ->shouldHaveType('Psr\Http\Message\StreamInterface'); } - function it_creates_a_stream_from_null() + public function it_creates_a_stream_from_null() { $this->createStream(null)->shouldHaveType('Psr\Http\Message\StreamInterface'); } + + public function it_creates_a_stream_from_non_seekable_resource() + { + $url = 'https://raw.githubusercontent.com/php-http/multipart-stream-builder/master/tests/Resources/httplug.png'; + $resource = fopen($url, 'r'); + $this->createStream($resource) + ->shouldHaveType('Psr\Http\Message\StreamInterface'); + } + + public function it_does_not_rewind_existing_stream() + { + $stream = new Stream(fopen('php://memory', 'rw')); + $stream->write('abcdef'); + $stream->seek(3); + + $this->createStream($stream) + ->shouldHaveContent('def'); + } + + public function it_does_not_rewind_existing_resource() + { + $resource = fopen('php://memory', 'rw'); + fwrite($resource, 'abcdef'); + fseek($resource, 3); + + $this->createStream($resource) + ->shouldHaveContent('def'); + } + + public function getMatchers(): array + { + return [ + 'haveContent' => function (StreamInterface $subject, $key) { + return $subject->getContents() === $key; + }, + ]; + } } diff --git a/spec/UriFactory/DiactorosUriFactorySpec.php b/spec/UriFactory/DiactorosUriFactorySpec.php index 823ad3c..d362dcd 100644 --- a/spec/UriFactory/DiactorosUriFactorySpec.php +++ b/spec/UriFactory/DiactorosUriFactorySpec.php @@ -2,22 +2,22 @@ namespace spec\Http\Message\UriFactory; -use Psr\Http\Message\UriInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\UriInterface; class DiactorosUriFactorySpec extends ObjectBehavior { use UriFactoryBehavior; - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\UriFactory\DiactorosUriFactory'); } /** - * TODO: Remove this when https://github.com/phpspec/phpspec/issues/825 is resolved + * TODO: Remove this when https://github.com/phpspec/phpspec/issues/825 is resolved. */ - function it_creates_a_uri_from_uri(UriInterface $uri) + public function it_creates_a_uri_from_uri(UriInterface $uri) { $this->createUri($uri)->shouldReturn($uri); } diff --git a/spec/UriFactory/GuzzleUriFactorySpec.php b/spec/UriFactory/GuzzleUriFactorySpec.php index 3cb19f5..a9b5510 100644 --- a/spec/UriFactory/GuzzleUriFactorySpec.php +++ b/spec/UriFactory/GuzzleUriFactorySpec.php @@ -2,22 +2,22 @@ namespace spec\Http\Message\UriFactory; -use Psr\Http\Message\UriInterface; use PhpSpec\ObjectBehavior; +use Psr\Http\Message\UriInterface; class GuzzleUriFactorySpec extends ObjectBehavior { use UriFactoryBehavior; - function it_is_initializable() + public function it_is_initializable() { $this->shouldHaveType('Http\Message\UriFactory\GuzzleUriFactory'); } /** - * TODO: Remove this when https://github.com/phpspec/phpspec/issues/825 is resolved + * TODO: Remove this when https://github.com/phpspec/phpspec/issues/825 is resolved. */ - function it_creates_a_uri_from_uri(UriInterface $uri) + public function it_creates_a_uri_from_uri(UriInterface $uri) { $this->createUri($uri)->shouldReturn($uri); } diff --git a/spec/UriFactory/SlimUriFactorySpec.php b/spec/UriFactory/SlimUriFactorySpec.php new file mode 100644 index 0000000..a486625 --- /dev/null +++ b/spec/UriFactory/SlimUriFactorySpec.php @@ -0,0 +1,27 @@ +shouldHaveType('Http\Message\UriFactory\SlimUriFactory'); + } + + /** + * TODO: Remove this when https://github.com/phpspec/phpspec/issues/825 is resolved. + */ + public function it_creates_a_uri_from_uri(UriInterface $uri) + { + $this->createUri($uri)->shouldReturn($uri); + } +} diff --git a/spec/UriFactory/UriFactoryBehavior.php b/spec/UriFactory/UriFactoryBehavior.php index bb2aeda..f7116bc 100644 --- a/spec/UriFactory/UriFactoryBehavior.php +++ b/spec/UriFactory/UriFactoryBehavior.php @@ -6,22 +6,22 @@ trait UriFactoryBehavior { - function it_is_a_uri_factory() + public function it_is_a_uri_factory() { $this->shouldImplement('Http\Message\UriFactory'); } - function it_creates_a_uri_from_string() + public function it_creates_a_uri_from_string() { $this->createUri('http://php-http.org')->shouldHaveType('Psr\Http\Message\UriInterface'); } - function it_creates_a_uri_from_uri(UriInterface $uri) + public function it_creates_a_uri_from_uri(UriInterface $uri) { $this->createUri($uri)->shouldReturn($uri); } - function it_throws_an_exception_when_uri_is_invalid() + public function it_throws_an_exception_when_uri_is_invalid() { $this->shouldThrow('InvalidArgumentException')->duringCreateUri(null); } diff --git a/src/Authentication.php b/src/Authentication.php index b50366f..0fe0cb3 100644 --- a/src/Authentication.php +++ b/src/Authentication.php @@ -5,18 +5,21 @@ use Psr\Http\Message\RequestInterface; /** - * Authenticate a PSR-7 Request. + * Add authentication information to a PSR-7 Request. * * @author Márk Sági-Kazár */ interface Authentication { /** - * Authenticates a request. + * Alter the request to add the authentication credentials. * - * @param RequestInterface $request + * To do that, the implementation might use pre-stored credentials or do + * separate HTTP requests to obtain a valid token. * - * @return RequestInterface + * @param RequestInterface $request The request without authentication information + * + * @return RequestInterface The request with added authentication information */ public function authenticate(RequestInterface $request); } diff --git a/src/Authentication/AutoBasicAuth.php b/src/Authentication/AutoBasicAuth.php index 7b6a429..6120016 100644 --- a/src/Authentication/AutoBasicAuth.php +++ b/src/Authentication/AutoBasicAuth.php @@ -27,9 +27,6 @@ public function __construct($shouldRremoveUserInfo = true) $this->shouldRemoveUserInfo = (bool) $shouldRremoveUserInfo; } - /** - * {@inheritdoc} - */ public function authenticate(RequestInterface $request) { $uri = $request->getUri(); diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 23618a5..85b13a2 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -32,9 +32,6 @@ public function __construct($username, $password) $this->password = $password; } - /** - * {@inheritdoc} - */ public function authenticate(RequestInterface $request) { $header = sprintf('Basic %s', base64_encode(sprintf('%s:%s', $this->username, $this->password))); diff --git a/src/Authentication/Bearer.php b/src/Authentication/Bearer.php index a8fb21a..287de2d 100644 --- a/src/Authentication/Bearer.php +++ b/src/Authentication/Bearer.php @@ -25,9 +25,6 @@ public function __construct($token) $this->token = $token; } - /** - * {@inheritdoc} - */ public function authenticate(RequestInterface $request) { $header = sprintf('Bearer %s', $this->token); diff --git a/src/Authentication/Chain.php b/src/Authentication/Chain.php index 71002bb..d1d36d5 100644 --- a/src/Authentication/Chain.php +++ b/src/Authentication/Chain.php @@ -33,9 +33,6 @@ public function __construct(array $authenticationChain = []) $this->authenticationChain = $authenticationChain; } - /** - * {@inheritdoc} - */ public function authenticate(RequestInterface $request) { foreach ($this->authenticationChain as $authentication) { diff --git a/src/Authentication/Header.php b/src/Authentication/Header.php new file mode 100644 index 0000000..5b1b2e0 --- /dev/null +++ b/src/Authentication/Header.php @@ -0,0 +1,33 @@ +name = $name; + $this->value = $value; + } + + public function authenticate(RequestInterface $request) + { + return $request->withHeader($this->name, $this->value); + } +} diff --git a/src/Authentication/Matching.php b/src/Authentication/Matching.php index 4b89b50..cbef52e 100644 --- a/src/Authentication/Matching.php +++ b/src/Authentication/Matching.php @@ -27,11 +27,7 @@ final class Matching implements Authentication */ private $matcher; - /** - * @param Authentication $authentication - * @param callable|null $matcher - */ - public function __construct(Authentication $authentication, callable $matcher = null) + public function __construct(Authentication $authentication, ?callable $matcher = null) { if (is_null($matcher)) { $matcher = function () { @@ -43,9 +39,6 @@ public function __construct(Authentication $authentication, callable $matcher = $this->matcher = new CallbackRequestMatcher($matcher); } - /** - * {@inheritdoc} - */ public function authenticate(RequestInterface $request) { if ($this->matcher->matches($request)) { @@ -58,8 +51,7 @@ public function authenticate(RequestInterface $request) /** * Creates a matching authentication for an URL. * - * @param Authentication $authentication - * @param string $url + * @param string $url * * @return self */ diff --git a/src/Authentication/QueryParam.php b/src/Authentication/QueryParam.php index 14b58ff..bc7b020 100644 --- a/src/Authentication/QueryParam.php +++ b/src/Authentication/QueryParam.php @@ -20,17 +20,11 @@ final class QueryParam implements Authentication */ private $params = []; - /** - * @param array $params - */ public function __construct(array $params) { $this->params = $params; } - /** - * {@inheritdoc} - */ public function authenticate(RequestInterface $request) { $uri = $request->getUri(); @@ -41,7 +35,7 @@ public function authenticate(RequestInterface $request) $params = array_merge($params, $this->params); - $query = http_build_query($params); + $query = http_build_query($params, '', '&'); $uri = $uri->withQuery($query); diff --git a/src/Authentication/RequestConditional.php b/src/Authentication/RequestConditional.php index 5477440..fefe44e 100644 --- a/src/Authentication/RequestConditional.php +++ b/src/Authentication/RequestConditional.php @@ -23,19 +23,12 @@ final class RequestConditional implements Authentication */ private $authentication; - /** - * @param RequestMatcher $requestMatcher - * @param Authentication $authentication - */ public function __construct(RequestMatcher $requestMatcher, Authentication $authentication) { $this->requestMatcher = $requestMatcher; $this->authentication = $authentication; } - /** - * {@inheritdoc} - */ public function authenticate(RequestInterface $request) { if ($this->requestMatcher->matches($request)) { diff --git a/src/Authentication/Wsse.php b/src/Authentication/Wsse.php index fbbde33..9191efe 100644 --- a/src/Authentication/Wsse.php +++ b/src/Authentication/Wsse.php @@ -22,25 +22,31 @@ final class Wsse implements Authentication */ private $password; + /** + * @var string + */ + private $hashAlgorithm; + /** * @param string $username * @param string $password + * @param string $hashAlgorithm To use a better hashing algorithm than the weak sha1, pass the algorithm to use, e.g. "sha512" */ - public function __construct($username, $password) + public function __construct($username, $password, $hashAlgorithm = 'sha1') { $this->username = $username; $this->password = $password; + if (false === in_array($hashAlgorithm, hash_algos())) { + throw new \InvalidArgumentException(sprintf('Unaccepted hashing algorithm: %s', $hashAlgorithm)); + } + $this->hashAlgorithm = $hashAlgorithm; } - /** - * {@inheritdoc} - */ public function authenticate(RequestInterface $request) { - // TODO: generate better nonce? $nonce = substr(md5(uniqid(uniqid().'_', true)), 0, 16); $created = date('c'); - $digest = base64_encode(sha1(base64_decode($nonce).$created.$this->password, true)); + $digest = base64_encode(hash($this->hashAlgorithm, base64_decode($nonce).$created.$this->password, true)); $wsse = sprintf( 'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"', diff --git a/src/Builder/ResponseBuilder.php b/src/Builder/ResponseBuilder.php index 0a198f4..4c3ecfc 100644 --- a/src/Builder/ResponseBuilder.php +++ b/src/Builder/ResponseBuilder.php @@ -18,8 +18,6 @@ class ResponseBuilder /** * Create builder for the given response. - * - * @param ResponseInterface $response */ public function __construct(ResponseInterface $response) { @@ -39,12 +37,12 @@ public function getResponse() /** * Add headers represented by an array of header lines. * - * @param string[] $headers Response headers as array of header lines. + * @param string[] $headers response headers as array of header lines * * @return $this * - * @throws \UnexpectedValueException For invalid header values. - * @throws \InvalidArgumentException For invalid status code arguments. + * @throws \UnexpectedValueException for invalid header values + * @throws \InvalidArgumentException for invalid status code arguments */ public function setHeadersFromArray(array $headers) { @@ -66,12 +64,12 @@ public function setHeadersFromArray(array $headers) /** * Add headers represented by a single string. * - * @param string $headers Response headers as single string. + * @param string $headers response headers as single string * * @return $this * * @throws \InvalidArgumentException if $headers is not a string on object with __toString() - * @throws \UnexpectedValueException For invalid header values. + * @throws \UnexpectedValueException for invalid header values */ public function setHeadersFromString($headers) { @@ -95,16 +93,16 @@ public function setHeadersFromString($headers) /** * Set response status from a status string. * - * @param string $statusLine Response status as a string. + * @param string $statusLine response status as a string * * @return $this * - * @throws \InvalidArgumentException For invalid status line. + * @throws \InvalidArgumentException for invalid status line */ public function setStatus($statusLine) { $parts = explode(' ', $statusLine, 3); - if (count($parts) < 2 || strpos(strtolower($parts[0]), 'http/') !== 0) { + if (count($parts) < 2 || 0 !== strpos(strtolower($parts[0]), 'http/')) { throw new \InvalidArgumentException( sprintf('"%s" is not a valid HTTP status line', $statusLine) ); @@ -121,22 +119,22 @@ public function setStatus($statusLine) /** * Add header represented by a string. * - * @param string $headerLine Response header as a string. + * @param string $headerLine response header as a string * * @return $this * - * @throws \InvalidArgumentException For invalid header names or values. + * @throws \InvalidArgumentException for invalid header names or values */ public function addHeader($headerLine) { $parts = explode(':', $headerLine, 2); - if (count($parts) !== 2) { + if (2 !== count($parts)) { throw new \InvalidArgumentException( sprintf('"%s" is not a valid HTTP header line', $headerLine) ); } - $name = trim(urldecode($parts[0])); - $value = trim(urldecode($parts[1])); + $name = trim($parts[0]); + $value = trim($parts[1]); if ($this->response->hasHeader($name)) { $this->response = $this->response->withAddedHeader($name, $value); } else { diff --git a/src/Cookie.php b/src/Cookie.php index 379089a..b79fe0a 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -56,14 +56,14 @@ final class Cookie /** * @param string $name * @param string|null $value - * @param int $maxAge + * @param int|null $maxAge * @param string|null $domain * @param string|null $path * @param bool $secure * @param bool $httpOnly * @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided. * - * @throws \InvalidArgumentException If name, value or max age is not valid. + * @throws \InvalidArgumentException if name, value or max age is not valid */ public function __construct( $name, @@ -73,7 +73,7 @@ public function __construct( $path = null, $secure = false, $httpOnly = false, - \DateTime $expires = null + ?\DateTime $expires = null ) { $this->validateName($name); $this->validateValue($value); @@ -89,6 +89,36 @@ public function __construct( $this->httpOnly = (bool) $httpOnly; } + /** + * Creates a new cookie without any attribute validation. + * + * @param string $name + * @param string|null $value + * @param int $maxAge + * @param string|null $domain + * @param string|null $path + * @param bool $secure + * @param bool $httpOnly + * @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided. + */ + public static function createWithoutValidation( + $name, + $value = null, + $maxAge = null, + $domain = null, + $path = null, + $secure = false, + $httpOnly = false, + ?\DateTime $expires = null + ) { + $cookie = new self('name', null, null, $domain, $path, $secure, $httpOnly, $expires); + $cookie->name = $name; + $cookie->value = $value; + $cookie->maxAge = $maxAge; + + return $cookie; + } + /** * Returns the name. * @@ -196,11 +226,9 @@ public function hasExpires() /** * Sets the expires. * - * @param \DateTime|null $expires - * * @return Cookie */ - public function withExpires(\DateTime $expires = null) + public function withExpires(?\DateTime $expires = null) { $new = clone $this; $new->expires = $expires; @@ -265,7 +293,7 @@ public function withDomain($domain) public function matchDomain($domain) { // Domain is not set or exact match - if (!$this->hasDomain() || strcasecmp($domain, $this->domain) === 0) { + if (!$this->hasDomain() || 0 === strcasecmp($domain, $this->domain)) { return true; } @@ -313,7 +341,7 @@ public function withPath($path) */ public function matchPath($path) { - return $this->path === $path || (strpos($path, $this->path.'/') === 0); + return $this->path === $path || (0 === strpos($path, rtrim($this->path, '/').'/')); } /** @@ -371,15 +399,31 @@ public function withHttpOnly($httpOnly) * * This does not compare the values, only name, domain and path. * - * @param Cookie $cookie - * * @return bool */ - public function match(Cookie $cookie) + public function match(self $cookie) { return $this->name === $cookie->name && $this->domain === $cookie->domain and $this->path === $cookie->path; } + /** + * Validates cookie attributes. + * + * @return bool + */ + public function isValid() + { + try { + $this->validateName($this->name); + $this->validateValue($this->value); + $this->validateMaxAge($this->maxAge); + } catch (\InvalidArgumentException $e) { + return false; + } + + return true; + } + /** * Validates the name attribute. * @@ -387,7 +431,7 @@ public function match(Cookie $cookie) * * @param string $name * - * @throws \InvalidArgumentException If the name is empty or contains invalid characters. + * @throws \InvalidArgumentException if the name is empty or contains invalid characters */ private function validateName($name) { @@ -408,7 +452,7 @@ private function validateName($name) * * @param string|null $value * - * @throws \InvalidArgumentException If the value contains invalid characters. + * @throws \InvalidArgumentException if the value contains invalid characters */ private function validateValue($value) { @@ -424,7 +468,7 @@ private function validateValue($value) * * @param int|null $maxAge * - * @throws \InvalidArgumentException If the Max-Age is not an empty or integer value. + * @throws \InvalidArgumentException if the Max-Age is not an empty or integer value */ private function validateMaxAge($maxAge) { @@ -467,9 +511,11 @@ private function normalizeDomain($domain) */ private function normalizePath($path) { - $path = rtrim($path, '/'); + if (null !== $path) { + $path = rtrim($path, '/'); + } - if (empty($path) or substr($path, 0, 1) !== '/') { + if (empty($path) or '/' !== substr($path, 0, 1)) { $path = '/'; } diff --git a/src/CookieJar.php b/src/CookieJar.php index ab267d3..159a99f 100644 --- a/src/CookieJar.php +++ b/src/CookieJar.php @@ -10,9 +10,9 @@ final class CookieJar implements \Countable, \IteratorAggregate { /** - * @var \SplObjectStorage + * @var \SplObjectStorage */ - protected $cookies; + private $cookies; public function __construct() { @@ -22,8 +22,6 @@ public function __construct() /** * Checks if there is a cookie. * - * @param Cookie $cookie - * * @return bool */ public function hasCookie(Cookie $cookie) @@ -33,8 +31,6 @@ public function hasCookie(Cookie $cookie) /** * Adds a cookie. - * - * @param Cookie $cookie */ public function addCookie(Cookie $cookie) { @@ -57,8 +53,6 @@ public function addCookie(Cookie $cookie) /** * Removes a cookie. - * - * @param Cookie $cookie */ public function removeCookie(Cookie $cookie) { @@ -82,8 +76,6 @@ public function getCookies() /** * Returns all matching cookies. * - * @param Cookie $cookie - * * @return Cookie[] */ public function getMatchingCookies(Cookie $cookie) @@ -98,11 +90,9 @@ public function getMatchingCookies(Cookie $cookie) /** * Finds matching cookies based on a callable. * - * @param callable $match - * * @return Cookie[] */ - protected function findMatchingCookies(callable $match) + private function findMatchingCookies(callable $match) { $cookies = []; @@ -202,17 +192,13 @@ public function clear() $this->cookies = new \SplObjectStorage(); } - /** - * {@inheritdoc} - */ + #[\ReturnTypeWillChange] public function count() { return $this->cookies->count(); } - /** - * {@inheritdoc} - */ + #[\ReturnTypeWillChange] public function getIterator() { return clone $this->cookies; diff --git a/src/CookieUtil.php b/src/CookieUtil.php new file mode 100644 index 0000000..44c5314 --- /dev/null +++ b/src/CookieUtil.php @@ -0,0 +1,53 @@ +message; } - /** - * {@inheritdoc} - */ - public function getProtocolVersion() + public function getProtocolVersion(): string { return $this->message->getProtocolVersion(); } - /** - * {@inheritdoc} - */ - public function withProtocolVersion($version) + public function withProtocolVersion(string $version): MessageInterface { $new = clone $this; $new->message = $this->message->withProtocolVersion($version); @@ -47,42 +39,27 @@ public function withProtocolVersion($version) return $new; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->message->getHeaders(); } - /** - * {@inheritdoc} - */ - public function hasHeader($header) + public function hasHeader(string $header): bool { return $this->message->hasHeader($header); } - /** - * {@inheritdoc} - */ - public function getHeader($header) + public function getHeader(string $header): array { return $this->message->getHeader($header); } - /** - * {@inheritdoc} - */ - public function getHeaderLine($header) + public function getHeaderLine(string $header): string { return $this->message->getHeaderLine($header); } - /** - * {@inheritdoc} - */ - public function withHeader($header, $value) + public function withHeader(string $header, $value): MessageInterface { $new = clone $this; $new->message = $this->message->withHeader($header, $value); @@ -90,10 +67,7 @@ public function withHeader($header, $value) return $new; } - /** - * {@inheritdoc} - */ - public function withAddedHeader($header, $value) + public function withAddedHeader(string $header, $value): MessageInterface { $new = clone $this; $new->message = $this->message->withAddedHeader($header, $value); @@ -101,10 +75,7 @@ public function withAddedHeader($header, $value) return $new; } - /** - * {@inheritdoc} - */ - public function withoutHeader($header) + public function withoutHeader(string $header): MessageInterface { $new = clone $this; $new->message = $this->message->withoutHeader($header); @@ -112,18 +83,12 @@ public function withoutHeader($header) return $new; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): StreamInterface { return $this->message->getBody(); } - /** - * {@inheritdoc} - */ - public function withBody(StreamInterface $body) + public function withBody(StreamInterface $body): MessageInterface { $new = clone $this; $new->message = $this->message->withBody($body); diff --git a/src/Decorator/RequestDecorator.php b/src/Decorator/RequestDecorator.php index 7c50e58..1392a76 100644 --- a/src/Decorator/RequestDecorator.php +++ b/src/Decorator/RequestDecorator.php @@ -16,12 +16,8 @@ trait RequestDecorator /** * Exchanges the underlying request with another. - * - * @param RequestInterface $request - * - * @return self */ - public function withRequest(RequestInterface $request) + public function withRequest(RequestInterface $request): RequestInterface { $new = clone $this; $new->message = $request; @@ -29,18 +25,12 @@ public function withRequest(RequestInterface $request) return $new; } - /** - * {@inheritdoc} - */ - public function getRequestTarget() + public function getRequestTarget(): string { return $this->message->getRequestTarget(); } - /** - * {@inheritdoc} - */ - public function withRequestTarget($requestTarget) + public function withRequestTarget(string $requestTarget): RequestInterface { $new = clone $this; $new->message = $this->message->withRequestTarget($requestTarget); @@ -48,18 +38,12 @@ public function withRequestTarget($requestTarget) return $new; } - /** - * {@inheritdoc} - */ - public function getMethod() + public function getMethod(): string { return $this->message->getMethod(); } - /** - * {@inheritdoc} - */ - public function withMethod($method) + public function withMethod(string $method): RequestInterface { $new = clone $this; $new->message = $this->message->withMethod($method); @@ -67,18 +51,12 @@ public function withMethod($method) return $new; } - /** - * {@inheritdoc} - */ - public function getUri() + public function getUri(): UriInterface { return $this->message->getUri(); } - /** - * {@inheritdoc} - */ - public function withUri(UriInterface $uri, $preserveHost = false) + public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface { $new = clone $this; $new->message = $this->message->withUri($uri, $preserveHost); diff --git a/src/Decorator/ResponseDecorator.php b/src/Decorator/ResponseDecorator.php index 82d9ae0..0b75438 100644 --- a/src/Decorator/ResponseDecorator.php +++ b/src/Decorator/ResponseDecorator.php @@ -15,12 +15,8 @@ trait ResponseDecorator /** * Exchanges the underlying response with another. - * - * @param ResponseInterface $response - * - * @return self */ - public function withResponse(ResponseInterface $response) + public function withResponse(ResponseInterface $response): ResponseInterface { $new = clone $this; $new->message = $response; @@ -28,18 +24,12 @@ public function withResponse(ResponseInterface $response) return $new; } - /** - * {@inheritdoc} - */ - public function getStatusCode() + public function getStatusCode(): int { return $this->message->getStatusCode(); } - /** - * {@inheritdoc} - */ - public function withStatus($code, $reasonPhrase = '') + public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface { $new = clone $this; $new->message = $this->message->withStatus($code, $reasonPhrase); @@ -47,10 +37,7 @@ public function withStatus($code, $reasonPhrase = '') return $new; } - /** - * {@inheritdoc} - */ - public function getReasonPhrase() + public function getReasonPhrase(): string { return $this->message->getReasonPhrase(); } diff --git a/src/Decorator/StreamDecorator.php b/src/Decorator/StreamDecorator.php index f405c7a..90d93b4 100644 --- a/src/Decorator/StreamDecorator.php +++ b/src/Decorator/StreamDecorator.php @@ -16,122 +16,77 @@ trait StreamDecorator */ protected $stream; - /** - * {@inheritdoc} - */ - public function __toString() + public function __toString(): string { return $this->stream->__toString(); } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { $this->stream->close(); } - /** - * {@inheritdoc} - */ public function detach() { return $this->stream->detach(); } - /** - * {@inheritdoc} - */ - public function getSize() + public function getSize(): ?int { return $this->stream->getSize(); } - /** - * {@inheritdoc} - */ - public function tell() + public function tell(): int { return $this->stream->tell(); } - /** - * {@inheritdoc} - */ - public function eof() + public function eof(): bool { return $this->stream->eof(); } - /** - * {@inheritdoc} - */ - public function isSeekable() + public function isSeekable(): bool { return $this->stream->isSeekable(); } - /** - * {@inheritdoc} - */ - public function seek($offset, $whence = SEEK_SET) + public function seek(int $offset, int $whence = SEEK_SET): void { $this->stream->seek($offset, $whence); } - /** - * {@inheritdoc} - */ - public function rewind() + public function rewind(): void { $this->stream->rewind(); } - /** - * {@inheritdoc} - */ - public function isWritable() + public function isWritable(): bool { return $this->stream->isWritable(); } - /** - * {@inheritdoc} - */ - public function write($string) + public function write(string $string): int { return $this->stream->write($string); } - /** - * {@inheritdoc} - */ - public function isReadable() + public function isReadable(): bool { return $this->stream->isReadable(); } - /** - * {@inheritdoc} - */ - public function read($length) + public function read(int $length): string { return $this->stream->read($length); } - /** - * {@inheritdoc} - */ - public function getContents() + public function getContents(): string { return $this->stream->getContents(); } - /** - * {@inheritdoc} - */ - public function getMetadata($key = null) + public function getMetadata(?string $key = null) { return $this->stream->getMetadata($key); } diff --git a/src/Encoding/ChunkStream.php b/src/Encoding/ChunkStream.php index 3e663b6..362ed78 100644 --- a/src/Encoding/ChunkStream.php +++ b/src/Encoding/ChunkStream.php @@ -9,26 +9,17 @@ */ class ChunkStream extends FilteredStream { - /** - * {@inheritdoc} - */ - public function getReadFilter() + protected function readFilter(): string { return 'chunk'; } - /** - * {@inheritdoc} - */ - public function getWriteFilter() + protected function writeFilter(): string { return 'dechunk'; } - /** - * {@inheritdoc} - */ - protected function fill() + protected function fill(): void { parent::fill(); diff --git a/src/Encoding/CompressStream.php b/src/Encoding/CompressStream.php index 08ea554..7e89388 100644 --- a/src/Encoding/CompressStream.php +++ b/src/Encoding/CompressStream.php @@ -2,6 +2,7 @@ namespace Http\Message\Encoding; +use Clue\StreamFilter as Filter; use Psr\Http\Message\StreamInterface; /** @@ -12,8 +13,7 @@ class CompressStream extends FilteredStream { /** - * @param StreamInterface $stream - * @param int $level + * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { @@ -21,21 +21,18 @@ public function __construct(StreamInterface $stream, $level = -1) throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } - parent::__construct($stream, ['window' => 15, 'level' => $level], ['window' => 15]); + parent::__construct($stream, ['window' => 15, 'level' => $level]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15]); } - /** - * {@inheritdoc} - */ - public function getReadFilter() + protected function readFilter(): string { return 'zlib.deflate'; } - /** - * {@inheritdoc} - */ - public function getWriteFilter() + protected function writeFilter(): string { return 'zlib.inflate'; } diff --git a/src/Encoding/DechunkStream.php b/src/Encoding/DechunkStream.php index 56d1208..c1fe3a6 100644 --- a/src/Encoding/DechunkStream.php +++ b/src/Encoding/DechunkStream.php @@ -11,18 +11,12 @@ */ class DechunkStream extends FilteredStream { - /** - * {@inheritdoc} - */ - public function getReadFilter() + protected function readFilter(): string { return 'dechunk'; } - /** - * {@inheritdoc} - */ - public function getWriteFilter() + protected function writeFilter(): string { return 'chunk'; } diff --git a/src/Encoding/DecompressStream.php b/src/Encoding/DecompressStream.php index 42c2838..aa3fdf0 100644 --- a/src/Encoding/DecompressStream.php +++ b/src/Encoding/DecompressStream.php @@ -2,6 +2,7 @@ namespace Http\Message\Encoding; +use Clue\StreamFilter as Filter; use Psr\Http\Message\StreamInterface; /** @@ -12,8 +13,7 @@ class DecompressStream extends FilteredStream { /** - * @param StreamInterface $stream - * @param int $level + * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { @@ -21,21 +21,18 @@ public function __construct(StreamInterface $stream, $level = -1) throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } - parent::__construct($stream, ['window' => 15], ['window' => 15, 'level' => $level]); + parent::__construct($stream, ['window' => 15]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15, 'level' => $level]); } - /** - * {@inheritdoc} - */ - public function getReadFilter() + protected function readFilter(): string { return 'zlib.inflate'; } - /** - * {@inheritdoc} - */ - public function getWriteFilter() + protected function writeFilter(): string { return 'zlib.deflate'; } diff --git a/src/Encoding/DeflateStream.php b/src/Encoding/DeflateStream.php index 1758d11..d8d8a8a 100644 --- a/src/Encoding/DeflateStream.php +++ b/src/Encoding/DeflateStream.php @@ -2,6 +2,7 @@ namespace Http\Message\Encoding; +use Clue\StreamFilter as Filter; use Psr\Http\Message\StreamInterface; /** @@ -12,26 +13,22 @@ class DeflateStream extends FilteredStream { /** - * @param StreamInterface $stream - * @param int $level + * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { - parent::__construct($stream, ['window' => -15, 'level' => $level], ['window' => -15]); + parent::__construct($stream, ['window' => -15, 'level' => $level]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15]); } - /** - * {@inheritdoc} - */ - public function getReadFilter() + protected function readFilter(): string { return 'zlib.deflate'; } - /** - * {@inheritdoc} - */ - public function getWriteFilter() + protected function writeFilter(): string { return 'zlib.inflate'; } diff --git a/src/Encoding/Filter/Chunk.php b/src/Encoding/Filter/Chunk.php index 0f8f53b..7a9e18f 100644 --- a/src/Encoding/Filter/Chunk.php +++ b/src/Encoding/Filter/Chunk.php @@ -9,10 +9,7 @@ */ class Chunk extends \php_user_filter { - /** - * {@inheritdoc} - */ - public function filter($in, $out, &$consumed, $closing) + public function filter($in, $out, &$consumed, $closing): int { while ($bucket = stream_bucket_make_writeable($in)) { $lenbucket = stream_bucket_new($this->stream, dechex($bucket->datalen)."\r\n"); diff --git a/src/Encoding/FilteredStream.php b/src/Encoding/FilteredStream.php index 38d6258..4bc44a6 100644 --- a/src/Encoding/FilteredStream.php +++ b/src/Encoding/FilteredStream.php @@ -13,9 +13,11 @@ */ abstract class FilteredStream implements StreamInterface { - const BUFFER_SIZE = 8192; - - use StreamDecorator; + use StreamDecorator { + rewind as private doRewind; + seek as private doSeek; + } + public const BUFFER_SIZE = 8192; /** * @var callable @@ -24,16 +26,22 @@ abstract class FilteredStream implements StreamInterface /** * @var resource + * + * @deprecated since version 1.5, will be removed in 2.0 */ protected $readFilter; /** * @var callable + * + * @deprecated since version 1.5, will be removed in 2.0 */ protected $writeFilterCallback; /** * @var resource + * + * @deprecated since version 1.5, will be removed in 2.0 */ protected $writeFilter; @@ -45,21 +53,29 @@ abstract class FilteredStream implements StreamInterface protected $buffer = ''; /** - * @param StreamInterface $stream - * @param mixed|null $readFilterOptions - * @param mixed|null $writeFilterOptions + * @param mixed|null $readFilterOptions + * @param mixed|null $writeFilterOptions deprecated since 1.5, will be removed in 2.0 */ public function __construct(StreamInterface $stream, $readFilterOptions = null, $writeFilterOptions = null) { - $this->readFilterCallback = Filter\fun($this->getReadFilter(), $readFilterOptions); - $this->writeFilterCallback = Filter\fun($this->getWriteFilter(), $writeFilterOptions); + if (null !== $readFilterOptions) { + $this->readFilterCallback = Filter\fun($this->readFilter(), $readFilterOptions); + } else { + $this->readFilterCallback = Filter\fun($this->readFilter()); + } + + if (null !== $writeFilterOptions) { + $this->writeFilterCallback = Filter\fun($this->writeFilter(), $writeFilterOptions); + + @trigger_error('The $writeFilterOptions argument is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); + } else { + $this->writeFilterCallback = Filter\fun($this->writeFilter()); + } + $this->stream = $stream; } - /** - * {@inheritdoc} - */ - public function read($length) + public function read(int $length): string { if (strlen($this->buffer) >= $length) { $read = substr($this->buffer, 0, $length); @@ -82,12 +98,9 @@ public function read($length) return $read.$this->read($length - strlen($read)); } - /** - * {@inheritdoc} - */ - public function eof() + public function eof(): bool { - return $this->stream->eof() && $this->buffer === ''; + return $this->stream->eof() && '' === $this->buffer; } /** @@ -97,27 +110,24 @@ public function eof() * This allow to get last data in the PHP buffer otherwise this * bug is present : https://bugs.php.net/bug.php?id=48725 */ - protected function fill() + protected function fill(): void { $readFilterCallback = $this->readFilterCallback; - $this->buffer .= $readFilterCallback($this->stream->read(self::BUFFER_SIZE)); + $this->buffer .= $readFilterCallback($this->stream->read(self::BUFFER_SIZE)); if ($this->stream->eof()) { $this->buffer .= $readFilterCallback(); } } - /** - * {@inheritdoc} - */ - public function getContents() + public function getContents(): string { $buffer = ''; while (!$this->eof()) { $buf = $this->read(self::BUFFER_SIZE); // Using a loose equality here to match on '' and false. - if ($buf == null) { + if (null == $buf) { break; } @@ -128,24 +138,77 @@ public function getContents() } /** - * {@inheritdoc} + * Always returns null because we can't tell the size of a stream when we filter. */ - public function __toString() + public function getSize(): ?int + { + return null; + } + + public function __toString(): string { return $this->getContents(); } + /** + * Filtered streams are not seekable. + * + * We would need to buffer and process everything to allow seeking. + */ + public function isSeekable(): bool + { + return false; + } + + /** + * Filtered streams are not seekable and can thus not be rewound. + */ + public function rewind(): void + { + @trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED); + $this->doRewind(); + } + + /** + * Filtered streams are not seekable. + */ + public function seek(int $offset, int $whence = SEEK_SET): void + { + @trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED); + $this->doSeek($offset, $whence); + } + /** * Returns the read filter name. * - * @return string + * @deprecated since version 1.5, will be removed in 2.0 + */ + public function getReadFilter(): string + { + @trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); + + return $this->readFilter(); + } + + /** + * Returns the write filter name. */ - abstract public function getReadFilter(); + abstract protected function readFilter(): string; /** * Returns the write filter name. * - * @return string + * @deprecated since version 1.5, will be removed in 2.0 + */ + public function getWriteFilter(): string + { + @trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); + + return $this->writeFilter(); + } + + /** + * Returns the write filter name. */ - abstract public function getWriteFilter(); + abstract protected function writeFilter(): string; } diff --git a/src/Encoding/GzipDecodeStream.php b/src/Encoding/GzipDecodeStream.php index d87073c..78ecc84 100644 --- a/src/Encoding/GzipDecodeStream.php +++ b/src/Encoding/GzipDecodeStream.php @@ -2,6 +2,7 @@ namespace Http\Message\Encoding; +use Clue\StreamFilter as Filter; use Psr\Http\Message\StreamInterface; /** @@ -12,8 +13,7 @@ class GzipDecodeStream extends FilteredStream { /** - * @param StreamInterface $stream - * @param int $level + * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { @@ -21,21 +21,18 @@ public function __construct(StreamInterface $stream, $level = -1) throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } - parent::__construct($stream, ['window' => 31], ['window' => 31, 'level' => $level]); + parent::__construct($stream, ['window' => 31]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31, 'level' => $level]); } - /** - * {@inheritdoc} - */ - public function getReadFilter() + protected function readFilter(): string { return 'zlib.inflate'; } - /** - * {@inheritdoc} - */ - public function getWriteFilter() + protected function writeFilter(): string { return 'zlib.deflate'; } diff --git a/src/Encoding/GzipEncodeStream.php b/src/Encoding/GzipEncodeStream.php index 477f052..7ffa499 100644 --- a/src/Encoding/GzipEncodeStream.php +++ b/src/Encoding/GzipEncodeStream.php @@ -2,6 +2,7 @@ namespace Http\Message\Encoding; +use Clue\StreamFilter as Filter; use Psr\Http\Message\StreamInterface; /** @@ -12,8 +13,7 @@ class GzipEncodeStream extends FilteredStream { /** - * @param StreamInterface $stream - * @param int $level + * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { @@ -21,21 +21,18 @@ public function __construct(StreamInterface $stream, $level = -1) throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } - parent::__construct($stream, ['window' => 31, 'level' => $level], ['window' => 31]); + parent::__construct($stream, ['window' => 31, 'level' => $level]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31]); } - /** - * {@inheritdoc} - */ - public function getReadFilter() + protected function readFilter(): string { return 'zlib.deflate'; } - /** - * {@inheritdoc} - */ - public function getWriteFilter() + protected function writeFilter(): string { return 'zlib.inflate'; } diff --git a/src/Encoding/InflateStream.php b/src/Encoding/InflateStream.php index e909db3..1abe88e 100644 --- a/src/Encoding/InflateStream.php +++ b/src/Encoding/InflateStream.php @@ -2,6 +2,7 @@ namespace Http\Message\Encoding; +use Clue\StreamFilter as Filter; use Psr\Http\Message\StreamInterface; /** @@ -12,8 +13,7 @@ class InflateStream extends FilteredStream { /** - * @param StreamInterface $stream - * @param int $level + * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { @@ -21,21 +21,18 @@ public function __construct(StreamInterface $stream, $level = -1) throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } - parent::__construct($stream, ['window' => -15], ['window' => -15, 'level' => $level]); + parent::__construct($stream, ['window' => -15]); + + // @deprecated will be removed in 2.0 + $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15, 'level' => $level]); } - /** - * {@inheritdoc} - */ - public function getReadFilter() + protected function readFilter(): string { return 'zlib.inflate'; } - /** - * {@inheritdoc} - */ - public function getWriteFilter() + protected function writeFilter(): string { return 'zlib.deflate'; } diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 0000000..80d4cd9 --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,10 @@ + + * + * The formatResponseForRequest method will be added to this interface in the next major version, replacing the formatRequest method. + * Meanwhile, callers SHOULD check the formatter for the existence of formatResponseForRequest and call that if available. + * + * @method string formatResponseForRequest(ResponseInterface $response, RequestInterface $request) Formats a response in context of its request. */ interface Formatter { /** * Formats a request. * - * @param RequestInterface $request - * * @return string */ public function formatRequest(RequestInterface $request); /** - * Formats a response. + * @deprecated since 1.13, use formatResponseForRequest() instead * - * @param ResponseInterface $response + * Formats a response. * * @return string */ diff --git a/src/Formatter/CurlCommandFormatter.php b/src/Formatter/CurlCommandFormatter.php new file mode 100644 index 0000000..1878cad --- /dev/null +++ b/src/Formatter/CurlCommandFormatter.php @@ -0,0 +1,97 @@ + + */ +class CurlCommandFormatter implements Formatter +{ + public function formatRequest(RequestInterface $request) + { + $command = sprintf('curl %s', escapeshellarg((string) $request->getUri()->withFragment(''))); + if ('1.0' === $request->getProtocolVersion()) { + $command .= ' --http1.0'; + } elseif ('2.0' === $request->getProtocolVersion()) { + $command .= ' --http2'; + } + + $method = strtoupper($request->getMethod()); + if ('HEAD' === $method) { + $command .= ' --head'; + } elseif ('GET' !== $method) { + $command .= ' --request '.$method; + } + + $command .= $this->getHeadersAsCommandOptions($request); + + $body = $request->getBody(); + if ($body->getSize() > 0) { + // escapeshellarg argument max length on Windows, but longer body in curl command would be impractical anyways + if ($body->getSize() > 8192) { + $data = '[too long stream omitted]'; + } elseif ($body->isSeekable()) { + $data = $body->__toString(); + $body->rewind(); + // all non-printable ASCII characters and except for \t, \r, \n + if (preg_match('/([\x00-\x09\x0C\x0E-\x1F\x7F])/', $data)) { + $data = '[binary stream omitted]'; + } + } else { + $data = '[non-seekable stream omitted]'; + } + $escapedData = @escapeshellarg($data); + if (empty($escapedData)) { + $escapedData = 'We couldn\'t not escape the data properly'; + } + + $command .= sprintf(' --data %s', $escapedData); + } + + return $command; + } + + public function formatResponse(ResponseInterface $response) + { + return ''; + } + + /** + * Formats a response in context of its request. + * + * @return string + */ + public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request) + { + return $this->formatResponse($response); + } + + /** + * @return string + */ + private function getHeadersAsCommandOptions(RequestInterface $request) + { + $command = ''; + foreach ($request->getHeaders() as $name => $values) { + if ('host' === strtolower($name) && $values[0] === $request->getUri()->getHost()) { + continue; + } + + if ('user-agent' === strtolower($name)) { + $command .= sprintf(' -A %s', escapeshellarg($values[0])); + + continue; + } + + $command .= sprintf(' -H %s', escapeshellarg($name.': '.$request->getHeaderLine($name))); + } + + return $command; + } +} diff --git a/src/Formatter/FullHttpMessageFormatter.php b/src/Formatter/FullHttpMessageFormatter.php new file mode 100644 index 0000000..116b2dd --- /dev/null +++ b/src/Formatter/FullHttpMessageFormatter.php @@ -0,0 +1,110 @@ + + */ +class FullHttpMessageFormatter implements Formatter +{ + /** + * The maximum length of the body. + * + * @var int|null + */ + private $maxBodyLength; + + /** + * @var string + */ + private $binaryDetectionRegex; + + /** + * @param int|null $maxBodyLength + * @param string $binaryDetectionRegex By default, this is all non-printable ASCII characters and except for \t, \r, \n + */ + public function __construct($maxBodyLength = 1000, string $binaryDetectionRegex = '/([\x00-\x09\x0C\x0E-\x1F\x7F])/') + { + $this->maxBodyLength = $maxBodyLength; + $this->binaryDetectionRegex = $binaryDetectionRegex; + } + + public function formatRequest(RequestInterface $request) + { + $message = sprintf( + "%s %s HTTP/%s\n", + $request->getMethod(), + $request->getRequestTarget(), + $request->getProtocolVersion() + ); + + foreach ($request->getHeaders() as $name => $values) { + $message .= $name.': '.implode(', ', $values)."\n"; + } + + return $this->addBody($request, $message); + } + + public function formatResponse(ResponseInterface $response) + { + $message = sprintf( + "HTTP/%s %s %s\n", + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + foreach ($response->getHeaders() as $name => $values) { + $message .= $name.': '.implode(', ', $values)."\n"; + } + + return $this->addBody($response, $message); + } + + /** + * Formats a response in context of its request. + * + * @return string + */ + public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request) + { + return $this->formatResponse($response); + } + + /** + * Add the message body if the stream is seekable. + * + * @param string $message + * + * @return string + */ + private function addBody(MessageInterface $request, $message) + { + $message .= "\n"; + $stream = $request->getBody(); + if (!$stream->isSeekable() || 0 === $this->maxBodyLength) { + // Do not read the stream + return $message; + } + + $data = $stream->__toString(); + $stream->rewind(); + + if (preg_match($this->binaryDetectionRegex, $data)) { + return $message.'[binary stream omitted]'; + } + + if (null === $this->maxBodyLength) { + return $message.$data; + } + + return $message.mb_substr($data, 0, $this->maxBodyLength); + } +} diff --git a/src/Formatter/SimpleFormatter.php b/src/Formatter/SimpleFormatter.php index b1fcabd..c8b0450 100644 --- a/src/Formatter/SimpleFormatter.php +++ b/src/Formatter/SimpleFormatter.php @@ -14,9 +14,6 @@ */ class SimpleFormatter implements Formatter { - /** - * {@inheritdoc} - */ public function formatRequest(RequestInterface $request) { return sprintf( @@ -27,9 +24,6 @@ public function formatRequest(RequestInterface $request) ); } - /** - * {@inheritdoc} - */ public function formatResponse(ResponseInterface $response) { return sprintf( @@ -39,4 +33,14 @@ public function formatResponse(ResponseInterface $response) $response->getProtocolVersion() ); } + + /** + * Formats a response in context of its request. + * + * @return string + */ + public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request) + { + return $this->formatResponse($response); + } } diff --git a/src/MessageFactory/DiactorosMessageFactory.php b/src/MessageFactory/DiactorosMessageFactory.php index 53f08ae..c13f573 100644 --- a/src/MessageFactory/DiactorosMessageFactory.php +++ b/src/MessageFactory/DiactorosMessageFactory.php @@ -2,15 +2,23 @@ namespace Http\Message\MessageFactory; -use Http\Message\StreamFactory\DiactorosStreamFactory; use Http\Message\MessageFactory; -use Zend\Diactoros\Request; -use Zend\Diactoros\Response; +use Http\Message\StreamFactory\DiactorosStreamFactory; +use Laminas\Diactoros\Request as LaminasRequest; +use Laminas\Diactoros\Response as LaminasResponse; +use Zend\Diactoros\Request as ZendRequest; +use Zend\Diactoros\Response as ZendResponse; + +if (!interface_exists(MessageFactory::class)) { + throw new \LogicException('You cannot use "Http\Message\MessageFactory\DiactorosMessageFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead'); +} /** * Creates Diactoros messages. * * @author GeLo + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory */ final class DiactorosMessageFactory implements MessageFactory { @@ -24,9 +32,6 @@ public function __construct() $this->streamFactory = new DiactorosStreamFactory(); } - /** - * {@inheritdoc} - */ public function createRequest( $method, $uri, @@ -34,7 +39,16 @@ public function createRequest( $body = null, $protocolVersion = '1.1' ) { - return (new Request( + if (class_exists(LaminasRequest::class)) { + return (new LaminasRequest( + $uri, + $method, + $this->streamFactory->createStream($body), + $headers + ))->withProtocolVersion($protocolVersion); + } + + return (new ZendRequest( $uri, $method, $this->streamFactory->createStream($body), @@ -42,9 +56,6 @@ public function createRequest( ))->withProtocolVersion($protocolVersion); } - /** - * {@inheritdoc} - */ public function createResponse( $statusCode = 200, $reasonPhrase = null, @@ -52,7 +63,15 @@ public function createResponse( $body = null, $protocolVersion = '1.1' ) { - return (new Response( + if (class_exists(LaminasResponse::class)) { + return (new LaminasResponse( + $this->streamFactory->createStream($body), + $statusCode, + $headers + ))->withProtocolVersion($protocolVersion); + } + + return (new ZendResponse( $this->streamFactory->createStream($body), $statusCode, $headers diff --git a/src/MessageFactory/GuzzleMessageFactory.php b/src/MessageFactory/GuzzleMessageFactory.php index 59eb655..8844f74 100644 --- a/src/MessageFactory/GuzzleMessageFactory.php +++ b/src/MessageFactory/GuzzleMessageFactory.php @@ -6,16 +6,19 @@ use GuzzleHttp\Psr7\Response; use Http\Message\MessageFactory; +if (!interface_exists(MessageFactory::class)) { + throw new \LogicException('You cannot use "Http\Message\MessageFactory\GuzzleMessageFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead'); +} + /** * Creates Guzzle messages. * * @author Márk Sági-Kazár + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory */ final class GuzzleMessageFactory implements MessageFactory { - /** - * {@inheritdoc} - */ public function createRequest( $method, $uri, @@ -32,9 +35,6 @@ public function createRequest( ); } - /** - * {@inheritdoc} - */ public function createResponse( $statusCode = 200, $reasonPhrase = null, diff --git a/src/MessageFactory/SlimMessageFactory.php b/src/MessageFactory/SlimMessageFactory.php new file mode 100644 index 0000000..118799c --- /dev/null +++ b/src/MessageFactory/SlimMessageFactory.php @@ -0,0 +1,72 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory + */ +final class SlimMessageFactory implements MessageFactory +{ + /** + * @var SlimStreamFactory + */ + private $streamFactory; + + /** + * @var SlimUriFactory + */ + private $uriFactory; + + public function __construct() + { + $this->streamFactory = new SlimStreamFactory(); + $this->uriFactory = new SlimUriFactory(); + } + + public function createRequest( + $method, + $uri, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ) { + return (new Request( + $method, + $this->uriFactory->createUri($uri), + new Headers($headers), + [], + [], + $this->streamFactory->createStream($body), + [] + ))->withProtocolVersion($protocolVersion); + } + + public function createResponse( + $statusCode = 200, + $reasonPhrase = null, + array $headers = [], + $body = null, + $protocolVersion = '1.1' + ) { + return (new Response( + $statusCode, + new Headers($headers), + $this->streamFactory->createStream($body) + ))->withProtocolVersion($protocolVersion); + } +} diff --git a/src/RequestMatcher/CallbackRequestMatcher.php b/src/RequestMatcher/CallbackRequestMatcher.php index 4d45e32..659e6e3 100644 --- a/src/RequestMatcher/CallbackRequestMatcher.php +++ b/src/RequestMatcher/CallbackRequestMatcher.php @@ -17,17 +17,11 @@ final class CallbackRequestMatcher implements RequestMatcher */ private $callback; - /** - * @param callable $callback - */ public function __construct(callable $callback) { $this->callback = $callback; } - /** - * {@inheritdoc} - */ public function matches(RequestInterface $request) { return (bool) call_user_func($this->callback, $request); diff --git a/src/RequestMatcher/RegexRequestMatcher.php b/src/RequestMatcher/RegexRequestMatcher.php index 91f3729..253d9bc 100644 --- a/src/RequestMatcher/RegexRequestMatcher.php +++ b/src/RequestMatcher/RegexRequestMatcher.php @@ -31,9 +31,6 @@ public function __construct($regex) $this->regex = $regex; } - /** - * {@inheritdoc} - */ public function matches(RequestInterface $request) { return (bool) preg_match($this->regex, (string) $request->getUri()); diff --git a/src/RequestMatcher/RequestMatcher.php b/src/RequestMatcher/RequestMatcher.php index e2aa021..4b28ccc 100644 --- a/src/RequestMatcher/RequestMatcher.php +++ b/src/RequestMatcher/RequestMatcher.php @@ -51,8 +51,6 @@ public function __construct($path = null, $host = null, $methods = [], $schemes } /** - * {@inheritdoc} - * * @api */ public function matches(RequestInterface $request) diff --git a/src/Stream/BufferedStream.php b/src/Stream/BufferedStream.php new file mode 100644 index 0000000..289ff8c --- /dev/null +++ b/src/Stream/BufferedStream.php @@ -0,0 +1,237 @@ +stream = $stream; + $this->size = $stream->getSize(); + + if ($useFileBuffer) { + $this->resource = fopen('php://temp/maxmemory:'.$memoryBuffer, 'rw+'); + } else { + $this->resource = fopen('php://memory', 'rw+'); + } + + if (false === $this->resource) { + throw new \RuntimeException('Cannot create a resource over temp or memory implementation'); + } + } + + public function __toString(): string + { + try { + $this->rewind(); + + return $this->getContents(); + } catch (\Throwable $throwable) { + return ''; + } + } + + public function close(): void + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot close on a detached stream'); + } + + $this->stream->close(); + fclose($this->resource); + } + + public function detach() + { + if (null === $this->resource) { + return null; + } + + // Force reading the remaining data of the stream + $this->getContents(); + + $resource = $this->resource; + $this->stream->close(); + $this->stream = null; + $this->resource = null; + + return $resource; + } + + public function getSize(): ?int + { + if (null === $this->resource) { + return null; + } + + if (null === $this->size && $this->stream->eof()) { + return $this->written; + } + + return $this->size; + } + + public function tell(): int + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot tell on a detached stream'); + } + + $tell = ftell($this->resource); + if (false === $tell) { + throw new \RuntimeException('ftell failed'); + } + + return $tell; + } + + public function eof(): bool + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot call eof on a detached stream'); + } + + // We are at the end only when both our resource and underlying stream are at eof + return $this->stream->eof() && (ftell($this->resource) === $this->written); + } + + public function isSeekable(): bool + { + return null !== $this->resource; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot seek on a detached stream'); + } + + fseek($this->resource, $offset, $whence); + } + + public function rewind(): void + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot rewind on a detached stream'); + } + + rewind($this->resource); + } + + public function isWritable(): bool + { + return false; + } + + public function write(string $string): int + { + throw new \RuntimeException('Cannot write on this stream'); + } + + public function isReadable(): bool + { + return null !== $this->resource; + } + + public function read(int $length): string + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot read on a detached stream'); + } + if ($length < 0) { + throw new \InvalidArgumentException('Can not read a negative amount of bytes'); + } + if (0 === $length) { + return ''; + } + + $read = ''; + + // First read from the resource + if (ftell($this->resource) !== $this->written) { + $read = fread($this->resource, $length); + } + if (false === $read) { + throw new \RuntimeException('Failed to read from resource'); + } + + $bytesRead = strlen($read); + + if ($bytesRead < $length) { + $streamRead = $this->stream->read($length - $bytesRead); + + // Write on the underlying stream what we read + $this->written += fwrite($this->resource, $streamRead); + $read .= $streamRead; + } + + return $read; + } + + public function getContents(): string + { + if (null === $this->resource) { + throw new \RuntimeException('Cannot read on a detached stream'); + } + + $read = ''; + + while (!$this->eof()) { + $read .= $this->read(8192); + } + + return $read; + } + + public function getMetadata(?string $key = null) + { + if (null === $this->resource) { + if (null === $key) { + return []; + } + + return null; + } + + $metadata = stream_get_meta_data($this->resource); + + if (null === $key) { + return $metadata; + } + + if (!array_key_exists($key, $metadata)) { + return null; + } + + return $metadata[$key]; + } +} diff --git a/src/StreamFactory/DiactorosStreamFactory.php b/src/StreamFactory/DiactorosStreamFactory.php index 21690de..1b8c6f4 100644 --- a/src/StreamFactory/DiactorosStreamFactory.php +++ b/src/StreamFactory/DiactorosStreamFactory.php @@ -3,37 +3,47 @@ namespace Http\Message\StreamFactory; use Http\Message\StreamFactory; +use Laminas\Diactoros\Stream as LaminasStream; use Psr\Http\Message\StreamInterface; -use Zend\Diactoros\Stream; +use Zend\Diactoros\Stream as ZendStream; + +if (!interface_exists(StreamFactory::class)) { + throw new \LogicException('You cannot use "Http\Message\MessageFactory\DiactorosStreamFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead'); +} /** * Creates Diactoros streams. * * @author Михаил Красильников + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory */ final class DiactorosStreamFactory implements StreamFactory { - /** - * {@inheritdoc} - */ public function createStream($body = null) { - if (!$body instanceof StreamInterface) { - if (is_resource($body)) { - $body = new Stream($body); - } else { - $stream = new Stream('php://memory', 'rw'); - - if (null !== $body) { - $stream->write((string) $body); - } + if ($body instanceof StreamInterface) { + return $body; + } - $body = $stream; + if (is_resource($body)) { + if (class_exists(LaminasStream::class)) { + return new LaminasStream($body); } + + return new ZendStream($body); + } + + if (class_exists(LaminasStream::class)) { + $stream = new LaminasStream('php://memory', 'rw'); + } else { + $stream = new ZendStream('php://memory', 'rw'); } - $body->rewind(); + if (null !== $body && '' !== $body) { + $stream->write((string) $body); + } - return $body; + return $stream; } } diff --git a/src/StreamFactory/GuzzleStreamFactory.php b/src/StreamFactory/GuzzleStreamFactory.php index 10f4d3f..f74bc5b 100644 --- a/src/StreamFactory/GuzzleStreamFactory.php +++ b/src/StreamFactory/GuzzleStreamFactory.php @@ -2,20 +2,29 @@ namespace Http\Message\StreamFactory; +use GuzzleHttp\Psr7\Utils; use Http\Message\StreamFactory; +if (!interface_exists(StreamFactory::class)) { + throw new \LogicException('You cannot use "Http\Message\MessageFactory\GuzzleStreamFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead'); +} + /** * Creates Guzzle streams. * * @author Михаил Красильников + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory */ final class GuzzleStreamFactory implements StreamFactory { - /** - * {@inheritdoc} - */ public function createStream($body = null) { + if (class_exists(Utils::class)) { + return Utils::streamFor($body); + } + + // legacy support for guzzle/psr7 1.* return \GuzzleHttp\Psr7\stream_for($body); } } diff --git a/src/StreamFactory/SlimStreamFactory.php b/src/StreamFactory/SlimStreamFactory.php new file mode 100644 index 0000000..4cfa9ae --- /dev/null +++ b/src/StreamFactory/SlimStreamFactory.php @@ -0,0 +1,40 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory + */ +final class SlimStreamFactory implements StreamFactory +{ + public function createStream($body = null) + { + if ($body instanceof StreamInterface) { + return $body; + } + + if (is_resource($body)) { + return new Stream($body); + } + + $resource = fopen('php://memory', 'r+'); + $stream = new Stream($resource); + if (null !== $body && '' !== $body) { + $stream->write((string) $body); + } + + return $stream; + } +} diff --git a/src/UriFactory/DiactorosUriFactory.php b/src/UriFactory/DiactorosUriFactory.php index 268c361..84d5397 100644 --- a/src/UriFactory/DiactorosUriFactory.php +++ b/src/UriFactory/DiactorosUriFactory.php @@ -3,25 +3,33 @@ namespace Http\Message\UriFactory; use Http\Message\UriFactory; +use Laminas\Diactoros\Uri as LaminasUri; use Psr\Http\Message\UriInterface; -use Zend\Diactoros\Uri; +use Zend\Diactoros\Uri as ZendUri; + +if (!interface_exists(UriFactory::class)) { + throw new \LogicException('You cannot use "Http\Message\MessageFactory\DiactorosUriFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead'); +} /** * Creates Diactoros URI. * * @author David de Boer + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory */ final class DiactorosUriFactory implements UriFactory { - /** - * {@inheritdoc} - */ public function createUri($uri) { if ($uri instanceof UriInterface) { return $uri; } elseif (is_string($uri)) { - return new Uri($uri); + if (class_exists(LaminasUri::class)) { + return new LaminasUri($uri); + } + + return new ZendUri($uri); } throw new \InvalidArgumentException('URI must be a string or UriInterface'); diff --git a/src/UriFactory/GuzzleUriFactory.php b/src/UriFactory/GuzzleUriFactory.php index 4c1c286..deac230 100644 --- a/src/UriFactory/GuzzleUriFactory.php +++ b/src/UriFactory/GuzzleUriFactory.php @@ -2,21 +2,30 @@ namespace Http\Message\UriFactory; -use GuzzleHttp\Psr7; +use GuzzleHttp\Psr7\Utils; use Http\Message\UriFactory; +use function GuzzleHttp\Psr7\uri_for; + +if (!interface_exists(UriFactory::class)) { + throw new \LogicException('You cannot use "Http\Message\MessageFactory\GuzzleUriFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead'); +} + /** * Creates Guzzle URI. * * @author David de Boer + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Guzzle PSR-17 factory */ final class GuzzleUriFactory implements UriFactory { - /** - * {@inheritdoc} - */ public function createUri($uri) { - return Psr7\uri_for($uri); + if (class_exists(Utils::class)) { + return Utils::uriFor($uri); + } + + return uri_for($uri); } } diff --git a/src/UriFactory/SlimUriFactory.php b/src/UriFactory/SlimUriFactory.php new file mode 100644 index 0000000..e4ff079 --- /dev/null +++ b/src/UriFactory/SlimUriFactory.php @@ -0,0 +1,34 @@ + + * + * @deprecated This will be removed in php-http/message2.0. Consider using the official Slim PSR-17 factory + */ +final class SlimUriFactory implements UriFactory +{ + public function createUri($uri) + { + if ($uri instanceof UriInterface) { + return $uri; + } + + if (is_string($uri)) { + return Uri::createFromString($uri); + } + + throw new \InvalidArgumentException('URI must be a string or UriInterface'); + } +}