diff --git a/.gitattributes b/.gitattributes index c47225a..9e28cc7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,15 +1,15 @@ -.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 -/behat.yml.dist export-ignore -/features/ export-ignore -/phpspec.ci.yml 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 export-ignore +/.scrutinizer.yml export-ignore +/.styleci.yml export-ignore +/behat.yml.dist export-ignore +/features/ export-ignore +/phpspec.ci.yml export-ignore +/phpspec.yml.dist export-ignore +/phpunit.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/spec/ export-ignore +/tests/ export-ignore 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/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..7585030 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,39 @@ +name: static + +on: + push: + branches: + - '*.x' + pull_request: + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Remove phpspec + run: composer remove --dev friends-of-phpspec/phpspec-code-coverage phpspec/phpspec + + - name: PHPStan + uses: docker://oskarstark/phpstan-ga + env: + REQUIRE_DEV: false + 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..145d475 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,125 @@ +name: tests + +on: + push: + branches: + - '*.x' + pull_request: + +jobs: + latest: + name: PHP ${{ matrix.php }} Latest + runs-on: ubuntu-latest + strategy: + matrix: + php: ['7.1', '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: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + coverage: none + + - name: Emulate PHP 8.3 + run: composer config platform.php 8.3.999 + if: matrix.php == '8.4' + + - name: Install PHP dependencies + run: composer update --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: composer test + + lowest: + name: PHP ${{ matrix.php }} Lowest + runs-on: ubuntu-latest + strategy: + matrix: + php: ['7.1', '7.4', '8.2', '8.3'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + coverage: none + + - name: Install dependencies + run: composer update --prefer-dist --prefer-stable --prefer-lowest --no-interaction --no-progress + + - name: Execute tests + run: composer test + + symfony: + name: Symfony ${{ matrix.symfony }} LTS + runs-on: ubuntu-latest + strategy: + matrix: + include: + - symfony: '4.4.*' + php-version: '7.1' + - symfony: '5.4.*' + php-version: '7.4' + - symfony: '6.4.*' + php-version: '8.2' + - symfony: '7.0.*' + php-version: '8.2' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer:v2 + coverage: none + + - name: Install dependencies + env: + SYMFONY_REQUIRE: ${{ matrix.symfony }} + run: | + composer global config --no-plugins allow-plugins.symfony/flex true + composer global require --no-progress --no-scripts --no-plugins symfony/flex + composer update --prefer-dist --no-interaction --prefer-stable --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:v2 + 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 + + - name: Upload coverage + run: | + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml diff --git a/.gitignore b/.gitignore index 16b4a20..5874147 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ +/.php-cs-fixer.php +/.php-cs-fixer.cache /behat.yml /build/ /composer.lock /phpspec.yml /phpunit.xml +/phpstan.neon /vendor/ + +.phpunit.result.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..92bc748 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,18 @@ +in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->name('*.php') +; + +$config = new PhpCsFixer\Config(); + +return $config + ->setRiskyAllowed(true) + ->setRules([ + '@Symfony' => true, + 'single_line_throw' => false, + ]) + ->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 @@ -in('src') + ->in('spec') +; +return PhpCsFixer\Config::create() + ->setRules([ + '@PSR2' => true, + '@Symfony' => true, + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'no_empty_phpdoc' => true, + 'phpdoc_to_comment' => false, + 'single_line_throw' => false, + ]) + ->setFinder($finder); diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index 5328b61..0000000 --- a/.styleci.yml +++ /dev/null @@ -1,14 +0,0 @@ -preset: symfony - -finder: - exclude: - - "spec" - path: - - "src" - - "tests" - -enabled: - - short_array_syntax - -disabled: - - phpdoc_annotation_without_dot # This is still buggy: https://github.com/symfony/symfony/pull/19198 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1e058db..0000000 --- a/.travis.yml +++ /dev/null @@ -1,68 +0,0 @@ -language: php - -sudo: false - -dist: trusty - -cache: - directories: - - $HOME/.composer/cache/files - -env: - global: - - TEST_COMMAND="composer test" - -branches: - except: - - /^analysis-.*$/ - -matrix: - fast_finish: true - include: - - php: 7.1 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" DEPENDENCIES="doctrine/instantiator:^1.1" - - # Test the latest stable release - - php: 5.4 - - php: 5.5 - - php: 5.6 - - php: 7.0 - - php: 7.1 - - php: 7.2 - - php: 7.3 - env: COVERAGE=true TEST_COMMAND="composer test-ci" DEPENDENCIES="henrikbjorn/phpspec-code-coverage:^1.0" - - # Test LTS versions - - php: 7.1 - env: DEPENDENCIES="dunglas/symfony-lock:^2" - - php: 7.1 - env: DEPENDENCIES="dunglas/symfony-lock:^3" - - php: 7.1 - env: DEPENDENCIES="dunglas/symfony-lock:^4" STABILITY="rc" - - # Latest dev release - - php: 7.1 - env: STABILITY="dev" - - allow_failures: - # Latest dev is allowed to fail. - - env: STABILITY="dev" - -before_install: - - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi - - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; - - if ! [ -z "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi; - -install: - - cat composer.json - # To be removed when this issue will be resolved: https://github.com/composer/composer/issues/5355 - - if [[ "$COMPOSER_FLAGS" == *"--prefer-lowest"* ]]; then composer update --prefer-dist --no-interaction --prefer-stable --quiet; fi - - composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction - -script: - - composer validate --strict --no-check-lock - - $TEST_COMMAND - -after_success: - - if [[ $COVERAGE = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ $COVERAGE = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 21462c7..31ea0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,117 @@ # Change Log +## 2.7.2 - 2024-09-24 + +- Updated code to not raise warnings for nullable parameters in PHP 8.4. +- Cleaned up PHPDoc comments. + +## 2.7.1 - 2023-11-30 + +- Allow installation with Symfony 7. + +## 2.7.0 - 2023-05-17 + +- Dropped `php-http/message-factory` from composer requirements as it is abandoned and this package does not actually use it. + +## 2.6.1 - 2023-04-14 + +- Allow installation with http-message (PSR-7) version 2 in addition to version 1. +- Support for PHP 8.2 + +## 2.6.0 - 2022-09-29 + +- [RedirectPlugin] Redirection of non GET/HEAD requests with a body now removes the body on follow-up requests, if the + HTTP method changes. To do this, the plugin needs to find a PSR-7 stream implementation. If none is found, you can + explicitly pass a PSR-17 StreamFactoryInterface in the `stream_factory` option. + To keep sending the body in all cases, set the `stream_factory` option to null explicitly. + +## 2.5.1 - 2022-09-29 + +### Fixed + +- [RedirectPlugin] Fixed handling of redirection to different domain with default port +- [RedirectPlugin] Fixed false positive circular detection in RedirectPlugin in cases when target location does not contain path + +## 2.5.0 - 2021-11-26 + +### Added + +- Support for Symfony 6 +- Support for PHP 8.1 + +### Changed + +- Dropped support for Symfony 2 and 3 - please keep using version 2.4.0 of this library if you can't update Symfony. + +## 2.4.0 - 2021-07-05 + +### Added + +- `strict` option to `RedirectPlugin` to allow preserving the request method on redirections with status 300, 301 and 302. + +## 2.3.0 - 2020-07-21 + +### Fixed + +- HttpMethodsClient with PSR RequestFactory +- Bug in the cookie plugin with empty cookies +- Bug when parsing null-valued date headers + +### Changed + +- Deprecation when constructing a HttpMethodsClient with PSR RequestFactory but without a StreamFactory + +## 2.2.1 - 2020-07-13 + +### Fixed + +- Support for PHP 8 +- Plugin callable phpdoc + +## 2.2.0 - 2020-07-02 + +### Added + +- Plugin client builder for making a `PluginClient` +- Support for the PSR-17 request factory in `HttpMethodsClient` + +### Changed + +- Restored support for `symfony/options-resolver: ^2.6` +- Consistent implementation of union type checking + +### Fixed + +- Memory leak when using the `PluginClient` with plugins + +## 2.1.0 - 2019-11-18 + +### Added + +- Support Symfony 5 + +## 2.0.0 - 2019-02-03 + +### Changed + +- HttpClientRouter now throws a HttpClientNoMatchException instead of a RequestException if it can not find a client for the request. +- RetryPlugin will only retry exceptions when there is no response, or a response in the 5xx HTTP code range. +- RetryPlugin also retries when no exception is thrown if the responses has HTTP code in the 5xx range. + The callbacks for exception handling have been renamed and callbacks for response handling have been added. +- Abstract method `HttpClientPool::chooseHttpClient()` has now an explicit return type (`Http\Client\Common\HttpClientPoolItem`) +- Interface method `Plugin::handleRequest(...)` has now an explicit return type (`Http\Promise\Promise`) +- Made classes final that are not intended to be extended. +- Added interfaces for BatchClient, HttpClientRouter and HttpMethodsClient. + (These interfaces use the `Interface` suffix to avoid name collisions.) +- Added an interface for HttpClientPool and moved the abstract class to the HttpClientPool sub namespace. +- AddPathPlugin: Do not add the prefix if the URL already has the same prefix. +- All exceptions in `Http\Client\Common\Exception` are final. + +### Removed + +- Deprecated option `debug_plugins` has been removed from `PluginClient` +- Deprecated options `decider` and `delay` have been removed from `RetryPlugin`, use `exception_decider` and `exception_delay` instead. + ## 1.11.0 - 2021-07-11 ### Changed @@ -28,7 +140,7 @@ ### Changed -- [RetryPlugin] Renamed the configuration options for the exception retry callback from `decider` to `exception_decider` +- RetryPlugin: Renamed the configuration options for the exception retry callback from `decider` to `exception_decider` and `delay` to `exception_delay`. The old names still work but are deprecated. ## 1.8.2 - 2018-12-14 @@ -59,10 +171,9 @@ - Decoder plugin will now remove header when there is no more encoding, instead of setting to an empty array - ## 1.7.0 - 2017-11-30 -### Added +### Added - Symfony 4 support @@ -82,12 +193,12 @@ ### Changed -- The `RetryPlugin` does now wait between retries. To disable/change this feature you must write something like: - +- The `RetryPlugin` does now wait between retries. To disable/change this feature you must write something like: + ```php -$plugin = new RetryPlugin(['delay' => function(RequestInterface $request, Exception $e, $retries) { - return 0; -}); +$plugin = new RetryPlugin(['delay' => function(RequestInterface $request, Exception $e, $retries) { + return 0; +}); ``` ### Deprecated diff --git a/README.md b/README.md index 017bfce..822d4cf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Latest Version](https://img.shields.io/github/release/php-http/client-common.svg?style=flat-square)](https://github.com/php-http/client-common/releases) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) -[![Build Status](https://img.shields.io/travis/php-http/client-common.svg?style=flat-square)](https://travis-ci.org/php-http/client-common) +[![Build Status](https://github.com/php-http/client-common/actions/workflows/tests.yml/badge.svg)](https://github.com/php-http/client-common/actions/workflows/tests.yml) [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/client-common.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client-common) [![Quality Score](https://img.shields.io/scrutinizer/g/php-http/client-common.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client-common) [![Total Downloads](https://img.shields.io/packagist/dt/php-http/client-common.svg?style=flat-square)](https://packagist.org/packages/php-http/client-common) diff --git a/composer.json b/composer.json index cce1f4f..25afb92 100644 --- a/composer.json +++ b/composer.json @@ -11,17 +11,26 @@ } ], "require": { - "php": "^5.4 || ^7.0", - "php-http/httplug": "^1.1", - "php-http/message-factory": "^1.0", + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", "php-http/message": "^1.6", - "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" }, "require-dev": { - "phpspec/phpspec": "^2.5 || ^3.4 || ^4.2", - "guzzlehttp/psr7": "^1.4" + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" }, "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", "php-http/logger-plugin": "PSR-3 Logger plugin", "php-http/cache-plugin": "PSR-6 Cache plugin", "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" @@ -31,13 +40,23 @@ "Http\\Client\\Common\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "spec\\Http\\Client\\Common\\": "spec/", + "Tests\\Http\\Client\\Common\\": "tests/" + } + }, "scripts": { - "test": "vendor/bin/phpspec run", - "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" + "test": [ + "vendor/bin/phpspec run", + "vendor/bin/phpunit" + ], + "test-ci": [ + "vendor/bin/phpspec run -c phpspec.ci.yml", + "vendor/bin/phpunit" + ] }, - "extra": { - "branch-alias": { - "dev-master": "1.10.x-dev" - } + "config": { + "sort-packages": true } } diff --git a/phpspec.ci.yml b/phpspec.ci.yml index 0838ef5..06a7469 100644 --- a/phpspec.ci.yml +++ b/phpspec.ci.yml @@ -4,7 +4,7 @@ suites: psr4_prefix: Http\Client\Common formatter.name: pretty extensions: - - PhpSpec\Extension\CodeCoverageExtension + FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: ~ code_coverage: format: clover output: build/coverage.xml diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..8886dea --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,80 @@ +parameters: + level: max + checkMissingIterableValueType: false + treatPhpDocTypesAsCertain: false + paths: + - src + ignoreErrors: + # Exception still thrown in PHP 8, not sure why phpstan complains + - + message: "#^Dead catch - UnexpectedValueException is never thrown in the try block\\.$#" + count: 2 + path: src/BatchResult.php + + - + message: "#^Method Http\\\\Client\\\\Common\\\\Plugin\\\\Journal\\:\\:addSuccess\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Plugin/Journal.php + + - + message: "#^Method Http\\\\Client\\\\Common\\\\Plugin\\\\Journal\\:\\:addFailure\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Plugin/Journal.php + + - + message: "#^Call to an undefined method Http\\\\Client\\\\HttpAsyncClient\\:\\:sendRequest\\(\\)\\.$#" + count: 1 + path: src/PluginClient.php + + - + message: "#^Method Http\\\\Client\\\\Common\\\\EmulatedHttpClient\\:\\:sendRequest\\(\\) should return Psr\\\\Http\\\\Message\\\\ResponseInterface but returns mixed\\.$#" + count: 1 + path: src/EmulatedHttpClient.php + + # we still support the obsolete RequestFactory for BC but do not require the package anymore + - + message: "#^Call to method createRequest\\(\\) on an unknown class Http\\\\Message\\\\RequestFactory\\.$#" + count: 1 + path: src/HttpMethodsClient.php + + - + message: "#^Class Http\\\\Message\\\\RequestFactory not found\\.$#" + count: 4 + path: src/HttpMethodsClient.php + + - + message: "#^Parameter \\$requestFactory of method Http\\\\Client\\\\Common\\\\HttpMethodsClient\\:\\:__construct\\(\\) has invalid type Http\\\\Message\\\\RequestFactory\\.$#" + count: 1 + path: src/HttpMethodsClient.php + + - + message: "#^Property Http\\\\Client\\\\Common\\\\HttpMethodsClient\\:\\:\\$requestFactory has unknown class Http\\\\Message\\\\RequestFactory as its type\\.$#" + count: 1 + path: src/HttpMethodsClient.php + + - + message: "#^Anonymous function should return Psr\\\\Http\\\\Message\\\\ResponseInterface but returns mixed\\.$#" + count: 1 + path: src/Plugin/RedirectPlugin.php + + # phpstan is confused by the optional dependencies. we check for existence first + - + message: "#^Method Http\\\\Client\\\\Common\\\\Plugin\\\\RedirectPlugin::guessStreamFactory\\(\\) should return Psr\\\\Http\\\\Message\\\\StreamFactoryInterface\\|null but returns Nyholm\\\\Psr7\\\\Factory\\\\Psr17Factory\\.$#" + count: 1 + path: src/Plugin/RedirectPlugin.php + + # phpstan is confused by the optional dependencies. we check for existence first + - + message: "#^Call to static method streamFor\\(\\) on an unknown class GuzzleHttp\\\\Psr7\\\\Utils\\.$#" + count: 1 + path: src/Plugin/RedirectPlugin.php + + - + message: "#^Method Http\\\\Client\\\\Common\\\\Plugin\\\\RetryPlugin\\:\\:retry\\(\\) should return Psr\\\\Http\\\\Message\\\\ResponseInterface but returns mixed\\.$#" + count: 1 + path: src/Plugin/RetryPlugin.php + + - + message: "#^Method Http\\\\Client\\\\Common\\\\PluginClient\\:\\:sendRequest\\(\\) should return Psr\\\\Http\\\\Message\\\\ResponseInterface but returns mixed\\.$#" + count: 2 + path: src/PluginClient.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d353b7c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + + + + + ./tests + + + diff --git a/spec/BatchClientSpec.php b/spec/BatchClientSpec.php index 962f00a..7dcb261 100644 --- a/spec/BatchClientSpec.php +++ b/spec/BatchClientSpec.php @@ -2,31 +2,35 @@ namespace spec\Http\Client\Common; +use Http\Client\Common\BatchClient; +use Http\Client\Common\BatchResult; +use Http\Client\Common\Exception\BatchException; +use Http\Client\Exception\HttpException; use Http\Client\HttpClient; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use PhpSpec\ObjectBehavior; class BatchClientSpec extends ObjectBehavior { - function let(HttpClient $client) + public function let(HttpClient $client) { - $this->beAnInstanceOf('Http\Client\Common\BatchClient', [$client]); + $this->beAnInstanceOf(BatchClient::class, [$client]); } - function it_send_multiple_request_using_send_request(HttpClient $client, RequestInterface $request1, RequestInterface $request2, ResponseInterface $response1, ResponseInterface $response2) + public function it_send_multiple_request_using_send_request(HttpClient $client, RequestInterface $request1, RequestInterface $request2, ResponseInterface $response1, ResponseInterface $response2) { $client->sendRequest($request1)->willReturn($response1); $client->sendRequest($request2)->willReturn($response2); - $this->sendRequests([$request1, $request2])->shouldReturnAnInstanceOf('Http\Client\Common\BatchResult'); + $this->sendRequests([$request1, $request2])->shouldReturnAnInstanceOf(BatchResult::class); } - function it_throw_batch_exception_if_one_or_more_request_failed(HttpClient $client, RequestInterface $request1, RequestInterface $request2, ResponseInterface $response) + public function it_throw_batch_exception_if_one_or_more_request_failed(HttpClient $client, RequestInterface $request1, RequestInterface $request2, ResponseInterface $response) { $client->sendRequest($request1)->willReturn($response); - $client->sendRequest($request2)->willThrow('Http\Client\Exception\HttpException'); + $client->sendRequest($request2)->willThrow(HttpException::class); - $this->shouldThrow('Http\Client\Common\Exception\BatchException')->duringSendRequests([$request1, $request2]); + $this->shouldThrow(BatchException::class)->duringSendRequests([$request1, $request2]); } } diff --git a/spec/BatchResultSpec.php b/spec/BatchResultSpec.php index c4618ac..775bb50 100644 --- a/spec/BatchResultSpec.php +++ b/spec/BatchResultSpec.php @@ -2,28 +2,29 @@ namespace spec\Http\Client\Common; +use Http\Client\Common\BatchResult; use Http\Client\Exception; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use PhpSpec\ObjectBehavior; class BatchResultSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { - $this->beAnInstanceOf('Http\Client\Common\BatchResult'); + $this->beAnInstanceOf(BatchResult::class); } - function it_is_immutable(RequestInterface $request, ResponseInterface $response) + public function it_is_immutable(RequestInterface $request, ResponseInterface $response) { $new = $this->addResponse($request, $response); $this->getResponses()->shouldReturn([]); - $new->shouldHaveType('Http\Client\Common\BatchResult'); + $new->shouldHaveType(BatchResult::class); $new->getResponses()->shouldReturn([$response]); } - function it_has_a_responses(RequestInterface $request, ResponseInterface $response) + public function it_has_a_responses(RequestInterface $request, ResponseInterface $response) { $new = $this->addResponse($request, $response); @@ -33,17 +34,17 @@ function it_has_a_responses(RequestInterface $request, ResponseInterface $respon $new->getResponses()->shouldReturn([$response]); } - function it_has_a_response_for_a_request(RequestInterface $request, ResponseInterface $response) + public function it_has_a_response_for_a_request(RequestInterface $request, ResponseInterface $response) { $new = $this->addResponse($request, $response); - $this->shouldThrow('UnexpectedValueException')->duringGetResponseFor($request); + $this->shouldThrow(\UnexpectedValueException::class)->duringGetResponseFor($request); $this->isSuccessful($request)->shouldReturn(false); $new->getResponseFor($request)->shouldReturn($response); $new->isSuccessful($request)->shouldReturn(true); } - function it_keeps_exception_after_add_request(RequestInterface $request1, Exception $exception, RequestInterface $request2, ResponseInterface $response) + public function it_keeps_exception_after_add_request(RequestInterface $request1, Exception $exception, RequestInterface $request2, ResponseInterface $response) { $new = $this->addException($request1, $exception); $new = $new->addResponse($request2, $response); diff --git a/spec/EmulatedHttpAsyncClientSpec.php b/spec/EmulatedHttpAsyncClientSpec.php index b7a9edd..d5d114a 100644 --- a/spec/EmulatedHttpAsyncClientSpec.php +++ b/spec/EmulatedHttpAsyncClientSpec.php @@ -2,51 +2,56 @@ namespace spec\Http\Client\Common; +use Http\Client\Common\EmulatedHttpAsyncClient; +use Http\Client\Exception\TransferException; +use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; +use Http\Client\Promise\HttpFulfilledPromise; +use Http\Client\Promise\HttpRejectedPromise; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use PhpSpec\ObjectBehavior; class EmulatedHttpAsyncClientSpec extends ObjectBehavior { - function let(HttpClient $httpClient) + public function let(HttpClient $httpClient) { $this->beConstructedWith($httpClient); } - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\EmulatedHttpAsyncClient'); + $this->shouldHaveType(EmulatedHttpAsyncClient::class); } - function it_is_an_http_client() + public function it_is_an_http_client() { - $this->shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } - function it_is_an_async_http_client() + public function it_is_an_async_http_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } - function it_emulates_a_successful_request( + public function it_emulates_a_successful_request( HttpClient $httpClient, RequestInterface $request, ResponseInterface $response ) { $httpClient->sendRequest($request)->willReturn($response); - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); + $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); } - function it_emulates_a_failed_request(HttpClient $httpClient, RequestInterface $request) + public function it_emulates_a_failed_request(HttpClient $httpClient, RequestInterface $request) { - $httpClient->sendRequest($request)->willThrow('Http\Client\Exception\TransferException'); + $httpClient->sendRequest($request)->willThrow(TransferException::class); - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); + $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpRejectedPromise::class); } - function it_decorates_the_underlying_client( + public function it_decorates_the_underlying_client( HttpClient $httpClient, RequestInterface $request, ResponseInterface $response diff --git a/spec/EmulatedHttpClientSpec.php b/spec/EmulatedHttpClientSpec.php index 976f772..cc9f91e 100644 --- a/spec/EmulatedHttpClientSpec.php +++ b/spec/EmulatedHttpClientSpec.php @@ -2,37 +2,39 @@ namespace spec\Http\Client\Common; +use Http\Client\Common\EmulatedHttpClient; +use Http\Client\Exception; use Http\Client\Exception\TransferException; -use Http\Client\HttpClient; use Http\Client\HttpAsyncClient; +use Http\Client\HttpClient; use Http\Promise\Promise; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use PhpSpec\ObjectBehavior; class EmulatedHttpClientSpec extends ObjectBehavior { - function let(HttpAsyncClient $httpAsyncClient) + public function let(HttpAsyncClient $httpAsyncClient) { $this->beConstructedWith($httpAsyncClient); } - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\EmulatedHttpClient'); + $this->shouldHaveType(EmulatedHttpClient::class); } - function it_is_an_http_client() + public function it_is_an_http_client() { - $this->shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } - function it_is_an_async_http_client() + public function it_is_an_async_http_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } - function it_emulates_a_successful_request( + public function it_emulates_a_successful_request( HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise, @@ -47,7 +49,7 @@ function it_emulates_a_successful_request( $this->sendRequest($request)->shouldReturn($response); } - function it_emulates_a_failed_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) + public function it_emulates_a_failed_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) { $promise->wait()->shouldBeCalled(); $promise->getState()->willReturn(Promise::REJECTED); @@ -55,10 +57,10 @@ function it_emulates_a_failed_request(HttpAsyncClient $httpAsyncClient, RequestI $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - $this->shouldThrow('Http\Client\Exception')->duringSendRequest($request); + $this->shouldThrow(Exception::class)->duringSendRequest($request); } - function it_decorates_the_underlying_client( + public function it_decorates_the_underlying_client( HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise diff --git a/spec/Exception/BatchExceptionSpec.php b/spec/Exception/BatchExceptionSpec.php index fa8d8d6..be72814 100644 --- a/spec/Exception/BatchExceptionSpec.php +++ b/spec/Exception/BatchExceptionSpec.php @@ -3,34 +3,35 @@ namespace spec\Http\Client\Common\Exception; use Http\Client\Common\BatchResult; +use Http\Client\Common\Exception\BatchException; use Http\Client\Exception; use PhpSpec\ObjectBehavior; class BatchExceptionSpec extends ObjectBehavior { - function let() + public function let() { $batchResult = new BatchResult(); $this->beConstructedWith($batchResult); } - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Exception\BatchException'); + $this->shouldHaveType(BatchException::class); } - function it_is_a_runtime_exception() + public function it_is_a_runtime_exception() { - $this->shouldHaveType('RuntimeException'); + $this->shouldHaveType(\RuntimeException::class); } - function it_is_an_exception() + public function it_is_an_exception() { - $this->shouldImplement('Http\Client\Exception'); + $this->shouldImplement(Exception::class); } - function it_has_a_batch_result() + public function it_has_a_batch_result() { - $this->getResult()->shouldHaveType('Http\Client\Common\BatchResult'); + $this->getResult()->shouldHaveType(BatchResult::class); } } diff --git a/spec/FlexibleHttpClientSpec.php b/spec/FlexibleHttpClientSpec.php index 70e6e4d..ec88da7 100644 --- a/spec/FlexibleHttpClientSpec.php +++ b/spec/FlexibleHttpClientSpec.php @@ -2,43 +2,44 @@ namespace spec\Http\Client\Common; +use Http\Client\Common\FlexibleHttpClient; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use Http\Promise\Promise; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use PhpSpec\ObjectBehavior; class FlexibleHttpClientSpec extends ObjectBehavior { - function let(HttpClient $httpClient) + public function let(HttpClient $httpClient) { $this->beConstructedWith($httpClient); } - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\FlexibleHttpClient'); + $this->shouldHaveType(FlexibleHttpClient::class); } - function it_is_an_http_client() + public function it_is_an_http_client() { - $this->shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } - function it_is_an_async_http_client() + public function it_is_an_async_http_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } - function it_throw_exception_if_invalid_client() + public function it_throw_type_error_if_invalid_client() { $this->beConstructedWith(null); - $this->shouldThrow('\LogicException')->duringInstantiation(); + $this->shouldThrow(\TypeError::class)->duringInstantiation(); } - function it_emulates_an_async_client( + public function it_emulates_an_async_client( HttpClient $httpClient, RequestInterface $syncRequest, ResponseInterface $syncResponse, @@ -53,11 +54,11 @@ function it_emulates_an_async_client( $this->sendRequest($syncRequest)->shouldReturn($syncResponse); $promise = $this->sendAsyncRequest($asyncRequest); - $promise->shouldHaveType('Http\Promise\Promise'); + $promise->shouldHaveType(Promise::class); $promise->wait()->shouldReturn($asyncResponse); } - function it_emulates_a_client( + public function it_emulates_a_client( HttpAsyncClient $httpAsyncClient, RequestInterface $asyncRequest, Promise $promise, @@ -75,10 +76,10 @@ function it_emulates_a_client( $this->sendRequest($syncRequest)->shouldReturn($syncResponse); } - function it_does_not_emulate_a_client($client, RequestInterface $syncRequest, RequestInterface $asyncRequest) + public function it_does_not_emulate_a_client($client, RequestInterface $syncRequest, RequestInterface $asyncRequest) { - $client->implement('Http\Client\HttpClient'); - $client->implement('Http\Client\HttpAsyncClient'); + $client->implement(HttpClient::class); + $client->implement(HttpAsyncClient::class); $client->sendRequest($syncRequest)->shouldBeCalled(); $client->sendRequest($asyncRequest)->shouldNotBeCalled(); diff --git a/spec/HttpClientPoolItemSpec.php b/spec/HttpClientPool/HttpClientPoolItemSpec.php similarity index 89% rename from spec/HttpClientPoolItemSpec.php rename to spec/HttpClientPool/HttpClientPoolItemSpec.php index 059ec8d..537378f 100644 --- a/spec/HttpClientPoolItemSpec.php +++ b/spec/HttpClientPool/HttpClientPoolItemSpec.php @@ -1,8 +1,9 @@ shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } public function it_is_an_async_http_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) @@ -53,7 +54,7 @@ public function it_disable_himself_on_send_request(HttpClient $httpClient, Reque $httpClient->sendRequest($request)->willThrow($exception); $this->shouldThrow($exception)->duringSendRequest($request); $this->isDisabled()->shouldReturn(true); - $this->shouldThrow('Http\Client\Exception\RequestException')->duringSendRequest($request); + $this->shouldThrow(RequestException::class)->duringSendRequest($request); } public function it_disable_himself_on_send_async_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request) @@ -63,9 +64,9 @@ public function it_disable_himself_on_send_async_request(HttpAsyncClient $httpAs $promise = new HttpRejectedPromise(new TransferException()); $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); + $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpRejectedPromise::class); $this->isDisabled()->shouldReturn(true); - $this->shouldThrow('Http\Client\Exception\RequestException')->duringSendAsyncRequest($request); + $this->shouldThrow(RequestException::class)->duringSendAsyncRequest($request); } public function it_reactivate_himself_on_send_request(HttpClient $httpClient, RequestInterface $request) @@ -87,9 +88,9 @@ public function it_reactivate_himself_on_send_async_request(HttpAsyncClient $htt $promise = new HttpRejectedPromise(new TransferException()); $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); + $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpRejectedPromise::class); $this->isDisabled()->shouldReturn(false); - $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); + $this->sendAsyncRequest($request)->shouldReturnAnInstanceOf(HttpRejectedPromise::class); } public function it_increments_request_count(HttpAsyncClient $httpAsyncClient, RequestInterface $request, ResponseInterface $response) @@ -156,7 +157,7 @@ public function getState() public function wait($unwrap = true) { - if ($this->state === Promise::FULFILLED) { + if (Promise::FULFILLED === $this->state) { if (!$unwrap) { return; } @@ -164,7 +165,7 @@ public function wait($unwrap = true) return $this->response; } - if ($this->state === Promise::REJECTED) { + if (Promise::REJECTED === $this->state) { if (!$unwrap) { return; } @@ -175,7 +176,7 @@ public function wait($unwrap = true) while (count($this->queue) > 0) { $callbacks = array_shift($this->queue); - if ($this->response !== null) { + if (null !== $this->response) { try { $this->response = $callbacks[0]($this->response); $this->exception = null; @@ -183,7 +184,7 @@ public function wait($unwrap = true) $this->response = null; $this->exception = $exception; } - } elseif ($this->exception !== null) { + } elseif (null !== $this->exception) { try { $this->response = $callbacks[1]($this->exception); $this->exception = null; @@ -194,7 +195,7 @@ public function wait($unwrap = true) } } - if ($this->response !== null) { + if (null !== $this->response) { $this->state = Promise::FULFILLED; if ($unwrap) { @@ -202,7 +203,7 @@ public function wait($unwrap = true) } } - if ($this->exception !== null) { + if (null !== $this->exception) { $this->state = Promise::REJECTED; if ($unwrap) { diff --git a/spec/HttpClientPool/LeastUsedClientPoolSpec.php b/spec/HttpClientPool/LeastUsedClientPoolSpec.php index a976c31..367288d 100644 --- a/spec/HttpClientPool/LeastUsedClientPoolSpec.php +++ b/spec/HttpClientPool/LeastUsedClientPoolSpec.php @@ -2,11 +2,13 @@ namespace spec\Http\Client\Common\HttpClientPool; -use Http\Client\Common\HttpClientPoolItem; +use Http\Client\Common\Exception\HttpClientNotFoundException; +use Http\Client\Common\HttpClientPool\HttpClientPoolItem; +use Http\Client\Common\HttpClientPool\LeastUsedClientPool; +use Http\Client\Exception\HttpException; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use Http\Promise\Promise; -use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Psr\Http\Message\RequestInterface; @@ -16,23 +18,23 @@ class LeastUsedClientPoolSpec extends ObjectBehavior { public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\HttpClientPool\LeastUsedClientPool'); + $this->shouldHaveType(LeastUsedClientPool::class); } public function it_is_an_http_client() { - $this->shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } public function it_is_an_async_http_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } public function it_throw_exception_with_no_client(RequestInterface $request) { - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendAsyncRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendAsyncRequest($request); } public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) @@ -55,27 +57,23 @@ public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, Request public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) { $this->addHttpClient($client); - $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + $client->sendRequest($request)->willThrow(HttpException::class); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); } public function it_reenable_client(HttpClient $client, RequestInterface $request) { $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); - $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + $client->sendRequest($request)->willThrow(HttpException::class); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); } public function it_uses_the_lowest_request_client(HttpClientPoolItem $client1, HttpClientPoolItem $client2, RequestInterface $request, ResponseInterface $response) { - if (extension_loaded('xdebug')) { - throw new SkippingException('This test fail when xdebug is enable on PHP < 7'); - } - $this->addHttpClient($client1); $this->addHttpClient($client2); diff --git a/spec/HttpClientPool/RandomClientPoolSpec.php b/spec/HttpClientPool/RandomClientPoolSpec.php index 4054d82..5cb34c3 100644 --- a/spec/HttpClientPool/RandomClientPoolSpec.php +++ b/spec/HttpClientPool/RandomClientPoolSpec.php @@ -2,7 +2,10 @@ namespace spec\Http\Client\Common\HttpClientPool; -use Http\Client\Common\HttpClientPoolItem; +use Http\Client\Common\Exception\HttpClientNotFoundException; +use Http\Client\Common\HttpClientPool\HttpClientPoolItem; +use Http\Client\Common\HttpClientPool\RandomClientPool; +use Http\Client\Exception\HttpException; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use Http\Promise\Promise; @@ -15,23 +18,23 @@ class RandomClientPoolSpec extends ObjectBehavior { public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\HttpClientPool\RandomClientPool'); + $this->shouldHaveType(RandomClientPool::class); } public function it_is_an_http_client() { - $this->shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } public function it_is_an_async_http_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } public function it_throw_exception_with_no_client(RequestInterface $request) { - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendAsyncRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendAsyncRequest($request); } public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) @@ -54,18 +57,18 @@ public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, Request public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) { $this->addHttpClient($client); - $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + $client->sendRequest($request)->willThrow(HttpException::class); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); } public function it_reenable_client(HttpClient $client, RequestInterface $request) { $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); - $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + $client->sendRequest($request)->willThrow(HttpException::class); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); } } diff --git a/spec/HttpClientPool/RoundRobinClientPoolSpec.php b/spec/HttpClientPool/RoundRobinClientPoolSpec.php index 48c2d01..5a272b2 100644 --- a/spec/HttpClientPool/RoundRobinClientPoolSpec.php +++ b/spec/HttpClientPool/RoundRobinClientPoolSpec.php @@ -2,7 +2,10 @@ namespace spec\Http\Client\Common\HttpClientPool; -use Http\Client\Common\HttpClientPoolItem; +use Http\Client\Common\Exception\HttpClientNotFoundException; +use Http\Client\Common\HttpClientPool\HttpClientPoolItem; +use Http\Client\Common\HttpClientPool\RoundRobinClientPool; +use Http\Client\Exception\HttpException; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use Http\Promise\Promise; @@ -15,23 +18,23 @@ class RoundRobinClientPoolSpec extends ObjectBehavior { public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\HttpClientPool\RoundRobinClientPool'); + $this->shouldHaveType(RoundRobinClientPool::class); } public function it_is_an_http_client() { - $this->shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } public function it_is_an_async_http_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } public function it_throw_exception_with_no_client(RequestInterface $request) { - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendAsyncRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendAsyncRequest($request); } public function it_sends_request(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) @@ -54,19 +57,19 @@ public function it_sends_async_request(HttpAsyncClient $httpAsyncClient, Request public function it_throw_exception_if_no_more_enable_client(HttpClient $client, RequestInterface $request) { $this->addHttpClient($client); - $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + $client->sendRequest($request)->willThrow(HttpException::class); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Common\Exception\HttpClientNotFoundException')->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); + $this->shouldThrow(HttpClientNotFoundException::class)->duringSendRequest($request); } public function it_reenable_client(HttpClient $client, RequestInterface $request) { $this->addHttpClient(new HttpClientPoolItem($client->getWrappedObject(), 0)); - $client->sendRequest($request)->willThrow('Http\Client\Exception\HttpException'); + $client->sendRequest($request)->willThrow(HttpException::class); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); - $this->shouldThrow('Http\Client\Exception\HttpException')->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); + $this->shouldThrow(HttpException::class)->duringSendRequest($request); } public function it_round_between_clients(HttpClient $client1, HttpClient $client2, RequestInterface $request, ResponseInterface $response) diff --git a/spec/HttpClientRouterSpec.php b/spec/HttpClientRouterSpec.php index 1119722..db2f112 100644 --- a/spec/HttpClientRouterSpec.php +++ b/spec/HttpClientRouterSpec.php @@ -2,32 +2,40 @@ namespace spec\Http\Client\Common; -use Http\Message\RequestMatcher; +use Http\Client\Common\Exception\HttpClientNoMatchException; +use Http\Client\Common\HttpClientRouter; +use Http\Client\Common\HttpClientRouterInterface; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; +use Http\Message\RequestMatcher; use Http\Promise\Promise; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use PhpSpec\ObjectBehavior; class HttpClientRouterSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() + { + $this->shouldHaveType(HttpClientRouter::class); + } + + public function it_is_an_http_client_router() { - $this->shouldHaveType('Http\Client\Common\HttpClientRouter'); + $this->shouldImplement(HttpClientRouterInterface::class); } - function it_is_an_http_client() + public function it_is_an_http_client() { - $this->shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } - function it_is_an_async_http_client() + public function it_is_an_async_http_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } - function it_send_request(RequestMatcher $matcher, HttpClient $client, RequestInterface $request, ResponseInterface $response) + public function it_send_request(RequestMatcher $matcher, HttpClient $client, RequestInterface $request, ResponseInterface $response) { $this->addClient($client, $matcher); $matcher->matches($request)->willReturn(true); @@ -36,7 +44,7 @@ function it_send_request(RequestMatcher $matcher, HttpClient $client, RequestInt $this->sendRequest($request)->shouldReturn($response); } - function it_send_async_request(RequestMatcher $matcher, HttpAsyncClient $client, RequestInterface $request, Promise $promise) + public function it_send_async_request(RequestMatcher $matcher, HttpAsyncClient $client, RequestInterface $request, Promise $promise) { $this->addClient($client, $matcher); $matcher->matches($request)->willReturn(true); @@ -45,19 +53,19 @@ function it_send_async_request(RequestMatcher $matcher, HttpAsyncClient $client, $this->sendAsyncRequest($request)->shouldReturn($promise); } - function it_throw_exception_on_send_request(RequestMatcher $matcher, HttpClient $client, RequestInterface $request) + public function it_throw_exception_on_send_request(RequestMatcher $matcher, HttpClient $client, RequestInterface $request) { $this->addClient($client, $matcher); $matcher->matches($request)->willReturn(false); - $this->shouldThrow('Http\Client\Exception\RequestException')->duringSendRequest($request); + $this->shouldThrow(HttpClientNoMatchException::class)->duringSendRequest($request); } - function it_throw_exception_on_send_async_request(RequestMatcher $matcher, HttpAsyncClient $client, RequestInterface $request) + public function it_throw_exception_on_send_async_request(RequestMatcher $matcher, HttpAsyncClient $client, RequestInterface $request) { $this->addClient($client, $matcher); $matcher->matches($request)->willReturn(false); - $this->shouldThrow('Http\Client\Exception\RequestException')->duringSendAsyncRequest($request); + $this->shouldThrow(HttpClientNoMatchException::class)->duringSendAsyncRequest($request); } } diff --git a/spec/HttpMethodsClientSpec.php b/spec/HttpMethodsClientSpec.php deleted file mode 100644 index 07c0b47..0000000 --- a/spec/HttpMethodsClientSpec.php +++ /dev/null @@ -1,116 +0,0 @@ -beAnInstanceOf( - 'spec\Http\Client\Common\HttpMethodsClientStub', [ - $client, - $messageFactory - ] - ); - } - - function it_sends_a_get_request() - { - $data = HttpMethodsClientStub::$requestData; - - $this->get($data['uri'], $data['headers'])->shouldReturn(true); - } - - function it_sends_a_head_request() - { - $data = HttpMethodsClientStub::$requestData; - - $this->head($data['uri'], $data['headers'])->shouldReturn(true); - } - - function it_sends_a_trace_request() - { - $data = HttpMethodsClientStub::$requestData; - - $this->trace($data['uri'], $data['headers'])->shouldReturn(true); - } - - function it_sends_a_post_request() - { - $data = HttpMethodsClientStub::$requestData; - - $this->post($data['uri'], $data['headers'], $data['body'])->shouldReturn(true); - } - - function it_sends_a_put_request() - { - $data = HttpMethodsClientStub::$requestData; - - $this->put($data['uri'], $data['headers'], $data['body'])->shouldReturn(true); - } - - function it_sends_a_patch_request() - { - $data = HttpMethodsClientStub::$requestData; - - $this->patch($data['uri'], $data['headers'], $data['body'])->shouldReturn(true); - } - - function it_sends_a_delete_request() - { - $data = HttpMethodsClientStub::$requestData; - - $this->delete($data['uri'], $data['headers'], $data['body'])->shouldReturn(true); - } - - function it_sends_a_options_request() - { - $data = HttpMethodsClientStub::$requestData; - - $this->options($data['uri'], $data['headers'], $data['body'])->shouldReturn(true); - } - - function it_sends_request_with_underlying_client(HttpClient $client, MessageFactory $messageFactory, RequestInterface $request, ResponseInterface $response) - { - $client->sendRequest($request)->shouldBeCalled()->willReturn($response); - - $this->beConstructedWith($client, $messageFactory); - $this->sendRequest($request)->shouldReturn($response); - } -} - -class HttpMethodsClientStub extends HttpMethodsClient -{ - public static $requestData = [ - 'uri' => '/uri', - 'headers' => [ - 'Content-Type' => 'text/plain', - ], - 'body' => 'body' - ]; - - /** - * {@inheritdoc} - */ - public function send($method, $uri, array $headers = [], $body = null) - { - if (in_array($method, ['GET', 'HEAD', 'TRACE'])) { - return $uri === self::$requestData['uri'] && - $headers === self::$requestData['headers'] && - is_null($body); - } - - return in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) && - $uri === self::$requestData['uri'] && - $headers === self::$requestData['headers'] && - $body === self::$requestData['body']; - } -} diff --git a/spec/Plugin/AddHostPluginSpec.php b/spec/Plugin/AddHostPluginSpec.php index caf4f21..e90ddfc 100644 --- a/spec/Plugin/AddHostPluginSpec.php +++ b/spec/Plugin/AddHostPluginSpec.php @@ -2,34 +2,34 @@ namespace spec\Http\Client\Common\Plugin; -use Http\Message\StreamFactory; -use Http\Message\UriFactory; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\AddHostPlugin; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class AddHostPluginSpec extends ObjectBehavior { - function let(UriInterface $uri) + public function let(UriInterface $uri) { $this->beConstructedWith($uri); } - function it_is_initializable(UriInterface $uri) + public function it_is_initializable(UriInterface $uri) { $uri->getHost()->shouldBeCalled()->willReturn('example.com'); - $this->shouldHaveType('Http\Client\Common\Plugin\AddHostPlugin'); + $this->shouldHaveType(AddHostPlugin::class); } - function it_is_a_plugin(UriInterface $uri) + public function it_is_a_plugin(UriInterface $uri) { $uri->getHost()->shouldBeCalled()->willReturn('example.com'); - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_adds_domain( + public function it_adds_domain( RequestInterface $request, UriInterface $host, UriInterface $uri @@ -47,10 +47,10 @@ function it_adds_domain( $uri->getHost()->shouldBeCalled()->willReturn(''); $this->beConstructedWith($host); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_replaces_domain( + public function it_replaces_domain( RequestInterface $request, UriInterface $host, UriInterface $uri @@ -67,10 +67,10 @@ function it_replaces_domain( $uri->withPort(8000)->shouldBeCalled()->willReturn($uri); $this->beConstructedWith($host, ['replace' => true]); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_nothing_when_domain_exists( + public function it_does_nothing_when_domain_exists( RequestInterface $request, UriInterface $host, UriInterface $uri @@ -79,6 +79,6 @@ function it_does_nothing_when_domain_exists( $uri->getHost()->shouldBeCalled()->willReturn('default.com'); $this->beConstructedWith($host); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } } diff --git a/spec/Plugin/AddPathPluginSpec.php b/spec/Plugin/AddPathPluginSpec.php index 6838ed8..4075622 100644 --- a/spec/Plugin/AddPathPluginSpec.php +++ b/spec/Plugin/AddPathPluginSpec.php @@ -2,34 +2,34 @@ namespace spec\Http\Client\Common\Plugin; -use Http\Message\StreamFactory; -use Http\Message\UriFactory; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\AddPathPlugin; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class AddPathPluginSpec extends ObjectBehavior { - function let(UriInterface $uri) + public function let(UriInterface $uri) { $this->beConstructedWith($uri); } - function it_is_initializable(UriInterface $uri) + public function it_is_initializable(UriInterface $uri) { $uri->getPath()->shouldBeCalled()->willReturn('/api'); - $this->shouldHaveType('Http\Client\Common\Plugin\AddPathPlugin'); + $this->shouldHaveType(AddPathPlugin::class); } - function it_is_a_plugin(UriInterface $uri) + public function it_is_a_plugin(UriInterface $uri) { $uri->getPath()->shouldBeCalled()->willReturn('/api'); - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_adds_path( + public function it_adds_path( RequestInterface $request, UriInterface $host, UriInterface $uri @@ -37,16 +37,16 @@ function it_adds_path( $host->getPath()->shouldBeCalled()->willReturn('/api'); $request->getUri()->shouldBeCalled()->willReturn($uri); - $request->withUri($uri)->shouldBeCalled()->willReturn($request); + $request->withUri($uri)->shouldBeCalledTimes(1)->willReturn($request); - $uri->withPath('/api/users')->shouldBeCalled()->willReturn($uri); + $uri->withPath('/api/users')->shouldBeCalledTimes(1)->willReturn($uri); $uri->getPath()->shouldBeCalled()->willReturn('/users'); $this->beConstructedWith($host); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_removes_ending_slashes( + public function it_removes_ending_slashes( RequestInterface $request, UriInterface $host, UriInterface $host2, @@ -63,14 +63,14 @@ function it_removes_ending_slashes( $uri->getPath()->shouldBeCalled()->willReturn('/users'); $this->beConstructedWith($host); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_throws_exception_on_empty_path(UriInterface $host) + public function it_throws_exception_on_empty_path(UriInterface $host) { $host->getPath()->shouldBeCalled()->willReturn(''); $this->beConstructedWith($host); - $this->shouldThrow('\LogicException')->duringInstantiation(); + $this->shouldThrow(\LogicException::class)->duringInstantiation(); } } diff --git a/spec/Plugin/AuthenticationPluginSpec.php b/spec/Plugin/AuthenticationPluginSpec.php index 02d1187..3191ade 100644 --- a/spec/Plugin/AuthenticationPluginSpec.php +++ b/spec/Plugin/AuthenticationPluginSpec.php @@ -2,34 +2,36 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\AuthenticationPlugin; use Http\Message\Authentication; use Http\Promise\Promise; -use Psr\Http\Message\RequestInterface; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; class AuthenticationPluginSpec extends ObjectBehavior { - function let(Authentication $authentication) + public function let(Authentication $authentication) { $this->beConstructedWith($authentication); } - function it_is_initializable(Authentication $authentication) + public function it_is_initializable(Authentication $authentication) { - $this->shouldHaveType('Http\Client\Common\Plugin\AuthenticationPlugin'); + $this->shouldHaveType(AuthenticationPlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_sends_an_authenticated_request(Authentication $authentication, RequestInterface $notAuthedRequest, RequestInterface $authedRequest, Promise $promise) + public function it_sends_an_authenticated_request(Authentication $authentication, RequestInterface $notAuthedRequest, RequestInterface $authedRequest, Promise $promise) { $authentication->authenticate($notAuthedRequest)->willReturn($authedRequest); - $next = function (RequestInterface $request) use($authedRequest, $promise) { + $next = function (RequestInterface $request) use ($authedRequest, $promise) { if (Argument::is($authedRequest->getWrappedObject())->scoreArgument($request)) { return $promise->getWrappedObject(); } diff --git a/spec/Plugin/BaseUriPluginSpec.php b/spec/Plugin/BaseUriPluginSpec.php index 2faf769..41d876b 100644 --- a/spec/Plugin/BaseUriPluginSpec.php +++ b/spec/Plugin/BaseUriPluginSpec.php @@ -2,36 +2,36 @@ namespace spec\Http\Client\Common\Plugin; -use Http\Message\StreamFactory; -use Http\Message\UriFactory; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\BaseUriPlugin; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; class BaseUriPluginSpec extends ObjectBehavior { - function let(UriInterface $uri) + public function let(UriInterface $uri) { $this->beConstructedWith($uri); } - function it_is_initializable(UriInterface $uri) + public function it_is_initializable(UriInterface $uri) { $uri->getHost()->shouldBeCalled()->willReturn('example.com'); $uri->getPath()->shouldBeCalled()->willReturn('/api'); - $this->shouldHaveType('Http\Client\Common\Plugin\BaseUriPlugin'); + $this->shouldHaveType(BaseUriPlugin::class); } - function it_is_a_plugin(UriInterface $uri) + public function it_is_a_plugin(UriInterface $uri) { $uri->getHost()->shouldBeCalled()->willReturn('example.com'); $uri->getPath()->shouldBeCalled()->willReturn('/api'); - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_adds_domain_and_path( + public function it_adds_domain_and_path( RequestInterface $request, UriInterface $host, UriInterface $uri @@ -52,10 +52,10 @@ function it_adds_domain_and_path( $uri->getPath()->shouldBeCalled()->willReturn('/users'); $this->beConstructedWith($host); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_adds_domain( + public function it_adds_domain( RequestInterface $request, UriInterface $host, UriInterface $uri @@ -74,10 +74,10 @@ function it_adds_domain( $uri->getHost()->shouldBeCalled()->willReturn(''); $this->beConstructedWith($host); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_replaces_domain_and_adds_path( + public function it_replaces_domain_and_adds_path( RequestInterface $request, UriInterface $host, UriInterface $uri @@ -97,6 +97,6 @@ function it_replaces_domain_and_adds_path( $uri->getPath()->shouldBeCalled()->willReturn('/users'); $this->beConstructedWith($host, ['replace' => true]); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } } diff --git a/spec/Plugin/ContentLengthPluginSpec.php b/spec/Plugin/ContentLengthPluginSpec.php index 4ec2ba7..39a8b71 100644 --- a/spec/Plugin/ContentLengthPluginSpec.php +++ b/spec/Plugin/ContentLengthPluginSpec.php @@ -2,37 +2,39 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\ContentLengthPlugin; use PhpSpec\Exception\Example\SkippingException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamInterface; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\StreamInterface; class ContentLengthPluginSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\ContentLengthPlugin'); + $this->shouldHaveType(ContentLengthPlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_adds_content_length_header(RequestInterface $request, StreamInterface $stream) + public function it_adds_content_length_header(RequestInterface $request, StreamInterface $stream) { $request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn($stream); $stream->getSize()->shouldBeCalled()->willReturn(100); $request->withHeader('Content-Length', '100')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterface $stream) + public function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterface $stream) { - if(defined('HHVM_VERSION')) { + if (defined('HHVM_VERSION')) { throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm'); } @@ -43,6 +45,6 @@ function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterfac $request->withBody(Argument::type('Http\Message\Encoding\ChunkStream'))->shouldBeCalled()->willReturn($request); $request->withAddedHeader('Transfer-Encoding', 'chunked')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } } diff --git a/spec/Plugin/ContentTypePluginSpec.php b/spec/Plugin/ContentTypePluginSpec.php index 3df7d87..a27d32a 100644 --- a/spec/Plugin/ContentTypePluginSpec.php +++ b/spec/Plugin/ContentTypePluginSpec.php @@ -2,108 +2,106 @@ namespace spec\Http\Client\Common\Plugin; -use PhpSpec\Exception\Example\SkippingException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamInterface; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\ContentTypePlugin; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; class ContentTypePluginSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\ContentTypePlugin'); + $this->shouldHaveType(ContentTypePlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_adds_json_content_type_header(RequestInterface $request) + public function it_adds_json_content_type_header(RequestInterface $request) { $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for(json_encode(['foo' => 'bar']))); $request->withHeader('Content-Type', 'application/json')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_adds_xml_content_type_header(RequestInterface $request) + public function it_adds_xml_content_type_header(RequestInterface $request) { $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_set_content_type_header(RequestInterface $request) + public function it_does_not_set_content_type_header(RequestInterface $request) { $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo')); $request->withHeader('Content-Type', null)->shouldNotBeCalled(); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_set_content_type_header_if_already_one(RequestInterface $request) + public function it_does_not_set_content_type_header_if_already_one(RequestInterface $request) { $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(true); $request->getBody()->shouldNotBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo')); $request->withHeader('Content-Type', null)->shouldNotBeCalled(); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_set_content_type_header_if_size_0_or_unknown(RequestInterface $request) + public function it_does_not_set_content_type_header_if_size_0_or_unknown(RequestInterface $request) { $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for()); $request->withHeader('Content-Type', null)->shouldNotBeCalled(); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_adds_xml_content_type_header_if_size_limit_is_not_reached_using_default_value(RequestInterface $request) + public function it_adds_xml_content_type_header_if_size_limit_is_not_reached_using_default_value(RequestInterface $request) { $this->beConstructedWith([ - 'skip_detection' => true + 'skip_detection' => true, ]); $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_adds_xml_content_type_header_if_size_limit_is_not_reached(RequestInterface $request) + public function it_adds_xml_content_type_header_if_size_limit_is_not_reached(RequestInterface $request) { $this->beConstructedWith([ 'skip_detection' => true, - 'size_limit' => 32000000 + 'size_limit' => 32000000, ]); $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_set_content_type_header_if_size_limit_is_reached(RequestInterface $request) + public function it_does_not_set_content_type_header_if_size_limit_is_reached(RequestInterface $request) { $this->beConstructedWith([ 'skip_detection' => true, - 'size_limit' => 8 + 'size_limit' => 8, ]); $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar')); $request->withHeader('Content-Type', null)->shouldNotBeCalled(); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - } diff --git a/spec/Plugin/CookiePluginSpec.php b/spec/Plugin/CookiePluginSpec.php index 2bb47e7..be5cd62 100644 --- a/spec/Plugin/CookiePluginSpec.php +++ b/spec/Plugin/CookiePluginSpec.php @@ -2,38 +2,41 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\CookiePlugin; +use Http\Client\Exception\TransferException; use Http\Client\Promise\HttpFulfilledPromise; +use Http\Client\Promise\HttpRejectedPromise; use Http\Message\Cookie; use Http\Message\CookieJar; use Http\Promise\Promise; +use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; -use Prophecy\Argument; class CookiePluginSpec extends ObjectBehavior { private $cookieJar; - function let() + public function let() { $this->cookieJar = new CookieJar(); $this->beConstructedWith($this->cookieJar); } - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\CookiePlugin'); + $this->shouldHaveType(CookiePlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_loads_cookie(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_loads_cookie(RequestInterface $request, UriInterface $uri, Promise $promise) { $cookie = new Cookie('name', 'value', 86400, 'test.com'); $this->cookieJar->addCookie($cookie); @@ -44,14 +47,10 @@ function it_loads_cookie(RequestInterface $request, UriInterface $uri, Promise $ $request->withAddedHeader('Cookie', 'name=value')->willReturn($request); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_combines_multiple_cookies_into_one_header(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_combines_multiple_cookies_into_one_header(RequestInterface $request, UriInterface $uri, Promise $promise) { $cookie = new Cookie('name', 'value', 86400, 'test.com'); $cookie2 = new Cookie('name2', 'value2', 86400, 'test.com'); @@ -65,28 +64,20 @@ function it_combines_multiple_cookies_into_one_header(RequestInterface $request, $request->withAddedHeader('Cookie', 'name=value; name2=value2')->willReturn($request); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_load_cookie_if_expired(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_does_not_load_cookie_if_expired(RequestInterface $request, UriInterface $uri, Promise $promise) { $cookie = new Cookie('name', 'value', null, 'test.com', false, false, null, (new \DateTime())->modify('-1 day')); $this->cookieJar->addCookie($cookie); $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_load_cookie_if_domain_does_not_match(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_does_not_load_cookie_if_domain_does_not_match(RequestInterface $request, UriInterface $uri, Promise $promise) { $cookie = new Cookie('name', 'value', 86400, 'test2.com'); $this->cookieJar->addCookie($cookie); @@ -96,14 +87,10 @@ function it_does_not_load_cookie_if_domain_does_not_match(RequestInterface $requ $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_load_cookie_on_hackish_domains(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_does_not_load_cookie_on_hackish_domains(RequestInterface $request, UriInterface $uri, Promise $promise) { $hackishDomains = [ 'hacktest.com', @@ -118,15 +105,11 @@ function it_does_not_load_cookie_on_hackish_domains(RequestInterface $request, U $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } } - function it_loads_cookie_on_subdomains(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_loads_cookie_on_subdomains(RequestInterface $request, UriInterface $uri, Promise $promise) { $cookie = new Cookie('name', 'value', 86400, 'test.com'); $this->cookieJar->addCookie($cookie); @@ -137,14 +120,10 @@ function it_loads_cookie_on_subdomains(RequestInterface $request, UriInterface $ $request->withAddedHeader('Cookie', 'name=value')->willReturn($request); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_load_cookie_if_path_does_not_match(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_does_not_load_cookie_if_path_does_not_match(RequestInterface $request, UriInterface $uri, Promise $promise) { $cookie = new Cookie('name', 'value', 86400, 'test.com', '/sub'); $this->cookieJar->addCookie($cookie); @@ -155,14 +134,10 @@ function it_does_not_load_cookie_if_path_does_not_match(RequestInterface $reques $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_load_cookie_when_cookie_is_secure(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_does_not_load_cookie_when_cookie_is_secure(RequestInterface $request, UriInterface $uri, Promise $promise) { $cookie = new Cookie('name', 'value', 86400, 'test.com', null, true); $this->cookieJar->addCookie($cookie); @@ -174,14 +149,10 @@ function it_does_not_load_cookie_when_cookie_is_secure(RequestInterface $request $request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled(); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_loads_cookie_when_cookie_is_secure(RequestInterface $request, UriInterface $uri, Promise $promise) + public function it_loads_cookie_when_cookie_is_secure(RequestInterface $request, UriInterface $uri, Promise $promise) { $cookie = new Cookie('name', 'value', 86400, 'test.com', null, true); $this->cookieJar->addCookie($cookie); @@ -193,14 +164,10 @@ function it_loads_cookie_when_cookie_is_secure(RequestInterface $request, UriInt $request->withAddedHeader('Cookie', 'name=value')->willReturn($request); - $this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) { - if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) { - return $promise->getWrappedObject(); - } - }, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_saves_cookie(RequestInterface $request, ResponseInterface $response, UriInterface $uri) + public function it_saves_cookie(RequestInterface $request, ResponseInterface $response, UriInterface $uri) { $next = function () use ($response) { return new HttpFulfilledPromise($response->getWrappedObject()); @@ -208,7 +175,7 @@ function it_saves_cookie(RequestInterface $request, ResponseInterface $response, $response->hasHeader('Set-Cookie')->willReturn(true); $response->getHeader('Set-Cookie')->willReturn([ - 'cookie=value; expires=Tuesday, 31-Mar-99 07:42:12 GMT; Max-Age=60; path=/; domain=test.com; secure; HttpOnly' + 'cookie=value; expires=Tuesday, 31-Mar-99 07:42:12 GMT; Max-Age=60; path=/; domain=test.com; secure; HttpOnly', ]); $request->getUri()->willReturn($uri); @@ -216,11 +183,11 @@ function it_saves_cookie(RequestInterface $request, ResponseInterface $response, $uri->getPath()->willReturn('/'); $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldHaveType('Http\Promise\Promise'); - $promise->wait()->shouldReturnAnInstanceOf('Psr\Http\Message\ResponseInterface'); + $promise->shouldHaveType(Promise::class); + $promise->wait()->shouldReturnAnInstanceOf(ResponseInterface::class); } - function it_throws_exception_on_invalid_expires_date( + public function it_throws_exception_on_invalid_expires_date( RequestInterface $request, ResponseInterface $response, UriInterface $uri @@ -231,7 +198,7 @@ function it_throws_exception_on_invalid_expires_date( $response->hasHeader('Set-Cookie')->willReturn(true); $response->getHeader('Set-Cookie')->willReturn([ - 'cookie=value; expires=i-am-an-invalid-date;' + 'cookie=value; expires=i-am-an-invalid-date;', ]); $request->getUri()->willReturn($uri); @@ -239,7 +206,7 @@ function it_throws_exception_on_invalid_expires_date( $uri->getPath()->willReturn('/'); $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow('Http\Client\Exception\TransferException')->duringWait(); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(TransferException::class)->duringWait(); } } diff --git a/spec/Plugin/DecoderPluginSpec.php b/spec/Plugin/DecoderPluginSpec.php index 7543027..c3731c8 100644 --- a/spec/Plugin/DecoderPluginSpec.php +++ b/spec/Plugin/DecoderPluginSpec.php @@ -2,35 +2,39 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\DecoderPlugin; use Http\Client\Promise\HttpFulfilledPromise; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; +use Http\Message\Encoding\DecompressStream; +use Http\Message\Encoding\GzipDecodeStream; use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; class DecoderPluginSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\DecoderPlugin'); + $this->shouldHaveType(DecoderPlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_decodes(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + public function it_decodes(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { - if(defined('HHVM_VERSION')) { + if (defined('HHVM_VERSION')) { throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm'); } $request->withHeader('TE', ['gzip', 'deflate', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate'])->shouldBeCalled()->willReturn($request); - $next = function () use($response) { + $next = function () use ($response) { return new HttpFulfilledPromise($response->getWrappedObject()); }; @@ -48,11 +52,11 @@ function it_decodes(RequestInterface $request, ResponseInterface $response, Stre $this->handleRequest($request, $next, function () {}); } - function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + public function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { $request->withHeader('TE', ['gzip', 'deflate', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate'])->shouldBeCalled()->willReturn($request); - $next = function () use($response) { + $next = function () use ($response) { return new HttpFulfilledPromise($response->getWrappedObject()); }; @@ -60,7 +64,7 @@ function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, $response->hasHeader('Content-Encoding')->willReturn(true); $response->getHeader('Content-Encoding')->willReturn(['gzip']); $response->getBody()->willReturn($stream); - $response->withBody(Argument::type('Http\Message\Encoding\GzipDecodeStream'))->willReturn($response); + $response->withBody(Argument::type(GzipDecodeStream::class))->willReturn($response); $response->withoutHeader('Content-Encoding')->willReturn($response); $stream->isReadable()->willReturn(true); @@ -70,11 +74,11 @@ function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, $this->handleRequest($request, $next, function () {}); } - function it_decodes_deflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + public function it_decodes_deflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { $request->withHeader('TE', ['gzip', 'deflate', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate'])->shouldBeCalled()->willReturn($request); - $next = function () use($response) { + $next = function () use ($response) { return new HttpFulfilledPromise($response->getWrappedObject()); }; @@ -82,7 +86,7 @@ function it_decodes_deflate(RequestInterface $request, ResponseInterface $respon $response->hasHeader('Content-Encoding')->willReturn(true); $response->getHeader('Content-Encoding')->willReturn(['deflate']); $response->getBody()->willReturn($stream); - $response->withBody(Argument::type('Http\Message\Encoding\DecompressStream'))->willReturn($response); + $response->withBody(Argument::type(DecompressStream::class))->willReturn($response); $response->withoutHeader('Content-Encoding')->willReturn($response); $stream->isReadable()->willReturn(true); @@ -92,13 +96,13 @@ function it_decodes_deflate(RequestInterface $request, ResponseInterface $respon $this->handleRequest($request, $next, function () {}); } - function it_does_not_decode_with_content_encoding(RequestInterface $request, ResponseInterface $response) + public function it_does_not_decode_with_content_encoding(RequestInterface $request, ResponseInterface $response) { $this->beConstructedWith(['use_content_encoding' => false]); $request->withHeader('TE', ['gzip', 'deflate', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate'])->shouldNotBeCalled(); - $next = function () use($response) { + $next = function () use ($response) { return new HttpFulfilledPromise($response->getWrappedObject()); }; diff --git a/spec/Plugin/ErrorPluginSpec.php b/spec/Plugin/ErrorPluginSpec.php index 20fcc25..7861246 100644 --- a/spec/Plugin/ErrorPluginSpec.php +++ b/spec/Plugin/ErrorPluginSpec.php @@ -2,82 +2,87 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Exception\ClientErrorException; +use Http\Client\Common\Exception\ServerErrorException; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\ErrorPlugin; use Http\Client\Promise\HttpFulfilledPromise; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; +use Http\Client\Promise\HttpRejectedPromise; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; class ErrorPluginSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { - $this->beAnInstanceOf('Http\Client\Common\Plugin\ErrorPlugin'); + $this->beAnInstanceOf(ErrorPlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_throw_client_error_exception_on_4xx_error(RequestInterface $request, ResponseInterface $response) + public function it_throw_client_error_exception_on_4xx_error(RequestInterface $request, ResponseInterface $response) { - $response->getStatusCode()->willReturn('400'); + $response->getStatusCode()->willReturn(400); $response->getReasonPhrase()->willReturn('Bad request'); - $next = function (RequestInterface $receivedRequest) use($request, $response) { + $next = function (RequestInterface $receivedRequest) use ($request, $response) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($response->getWrappedObject()); } }; $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow('Http\Client\Common\Exception\ClientErrorException')->duringWait(); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(ClientErrorException::class)->duringWait(); } - function it_does_not_throw_client_error_exception_on_4xx_error_if_only_server_exception(RequestInterface $request, ResponseInterface $response) + public function it_does_not_throw_client_error_exception_on_4xx_error_if_only_server_exception(RequestInterface $request, ResponseInterface $response) { $this->beConstructedWith(['only_server_exception' => true]); - $response->getStatusCode()->willReturn('400'); + $response->getStatusCode()->willReturn(400); $response->getReasonPhrase()->willReturn('Bad request'); - $next = function (RequestInterface $receivedRequest) use($request, $response) { + $next = function (RequestInterface $receivedRequest) use ($request, $response) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($response->getWrappedObject()); } }; - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); + $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); } - function it_throw_server_error_exception_on_5xx_error(RequestInterface $request, ResponseInterface $response) + public function it_throw_server_error_exception_on_5xx_error(RequestInterface $request, ResponseInterface $response) { - $response->getStatusCode()->willReturn('500'); + $response->getStatusCode()->willReturn(500); $response->getReasonPhrase()->willReturn('Server error'); - $next = function (RequestInterface $receivedRequest) use($request, $response) { + $next = function (RequestInterface $receivedRequest) use ($request, $response) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($response->getWrappedObject()); } }; $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow('Http\Client\Common\Exception\ServerErrorException')->duringWait(); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(ServerErrorException::class)->duringWait(); } - function it_returns_response(RequestInterface $request, ResponseInterface $response) + public function it_returns_response(RequestInterface $request, ResponseInterface $response) { - $response->getStatusCode()->willReturn('200'); + $response->getStatusCode()->willReturn(200); - $next = function (RequestInterface $receivedRequest) use($request, $response) { + $next = function (RequestInterface $receivedRequest) use ($request, $response) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($response->getWrappedObject()); } }; - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); + $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); } } diff --git a/spec/Plugin/HeaderAppendPluginSpec.php b/spec/Plugin/HeaderAppendPluginSpec.php index 24b8565..9325069 100644 --- a/spec/Plugin/HeaderAppendPluginSpec.php +++ b/spec/Plugin/HeaderAppendPluginSpec.php @@ -2,36 +2,35 @@ namespace spec\Http\Client\Common\Plugin; -use PhpSpec\Exception\Example\SkippingException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamInterface; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\HeaderAppendPlugin; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; class HeaderAppendPluginSpec extends ObjectBehavior { public function it_is_initializable() { $this->beConstructedWith([]); - $this->shouldHaveType('Http\Client\Common\Plugin\HeaderAppendPlugin'); + $this->shouldHaveType(HeaderAppendPlugin::class); } public function it_is_a_plugin() { $this->beConstructedWith([]); - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } public function it_appends_the_header(RequestInterface $request) { $this->beConstructedWith([ - 'foo'=>'bar', - 'baz'=>'qux' + 'foo' => 'bar', + 'baz' => 'qux', ]); $request->withAddedHeader('foo', 'bar')->shouldBeCalled()->willReturn($request); $request->withAddedHeader('baz', 'qux')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } } diff --git a/spec/Plugin/HeaderDefaultsPluginSpec.php b/spec/Plugin/HeaderDefaultsPluginSpec.php index 341f1a5..5a50a9c 100644 --- a/spec/Plugin/HeaderDefaultsPluginSpec.php +++ b/spec/Plugin/HeaderDefaultsPluginSpec.php @@ -2,37 +2,36 @@ namespace spec\Http\Client\Common\Plugin; -use PhpSpec\Exception\Example\SkippingException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamInterface; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\HeaderDefaultsPlugin; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; class HeaderDefaultsPluginSpec extends ObjectBehavior { public function it_is_initializable() { $this->beConstructedWith([]); - $this->shouldHaveType('Http\Client\Common\Plugin\HeaderDefaultsPlugin'); + $this->shouldHaveType(HeaderDefaultsPlugin::class); } public function it_is_a_plugin() { $this->beConstructedWith([]); - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } public function it_sets_the_default_header(RequestInterface $request) { $this->beConstructedWith([ 'foo' => 'bar', - 'baz' => 'qux' + 'baz' => 'qux', ]); $request->hasHeader('foo')->shouldBeCalled()->willReturn(false); $request->withHeader('foo', 'bar')->shouldBeCalled()->willReturn($request); $request->hasHeader('baz')->shouldBeCalled()->willReturn(true); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } } diff --git a/spec/Plugin/HeaderRemovePluginSpec.php b/spec/Plugin/HeaderRemovePluginSpec.php index 9ea2752..3f60359 100644 --- a/spec/Plugin/HeaderRemovePluginSpec.php +++ b/spec/Plugin/HeaderRemovePluginSpec.php @@ -2,31 +2,30 @@ namespace spec\Http\Client\Common\Plugin; -use PhpSpec\Exception\Example\SkippingException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamInterface; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\HeaderRemovePlugin; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; class HeaderRemovePluginSpec extends ObjectBehavior { public function it_is_initializable() { $this->beConstructedWith([]); - $this->shouldHaveType('Http\Client\Common\Plugin\HeaderRemovePlugin'); + $this->shouldHaveType(HeaderRemovePlugin::class); } public function it_is_a_plugin() { $this->beConstructedWith([]); - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } public function it_removes_the_header(RequestInterface $request) { $this->beConstructedWith([ 'foo', - 'baz' + 'baz', ]); $request->hasHeader('foo')->shouldBeCalled()->willReturn(false); @@ -34,6 +33,6 @@ public function it_removes_the_header(RequestInterface $request) $request->hasHeader('baz')->shouldBeCalled()->willReturn(true); $request->withoutHeader('baz')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } } diff --git a/spec/Plugin/HeaderSetPluginSpec.php b/spec/Plugin/HeaderSetPluginSpec.php index f4a340c..b152567 100644 --- a/spec/Plugin/HeaderSetPluginSpec.php +++ b/spec/Plugin/HeaderSetPluginSpec.php @@ -2,36 +2,35 @@ namespace spec\Http\Client\Common\Plugin; -use PhpSpec\Exception\Example\SkippingException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamInterface; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\HeaderSetPlugin; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; class HeaderSetPluginSpec extends ObjectBehavior { public function it_is_initializable() { $this->beConstructedWith([]); - $this->shouldHaveType('Http\Client\Common\Plugin\HeaderSetPlugin'); + $this->shouldHaveType(HeaderSetPlugin::class); } public function it_is_a_plugin() { $this->beConstructedWith([]); - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } public function it_set_the_header(RequestInterface $request) { $this->beConstructedWith([ - 'foo'=>'bar', - 'baz'=>'qux' + 'foo' => 'bar', + 'baz' => 'qux', ]); $request->withHeader('foo', 'bar')->shouldBeCalled()->willReturn($request); $request->withHeader('baz', 'qux')->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } } diff --git a/spec/Plugin/HistoryPluginSpec.php b/spec/Plugin/HistoryPluginSpec.php index 24e7f51..cd8459b 100644 --- a/spec/Plugin/HistoryPluginSpec.php +++ b/spec/Plugin/HistoryPluginSpec.php @@ -2,35 +2,36 @@ namespace spec\Http\Client\Common\Plugin; -use Http\Client\Exception\TransferException; +use Http\Client\Common\Plugin; use Http\Client\Common\Plugin\Journal; +use Http\Client\Exception\TransferException; use Http\Client\Promise\HttpFulfilledPromise; use Http\Client\Promise\HttpRejectedPromise; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; class HistoryPluginSpec extends ObjectBehavior { - function let(Journal $journal) + public function let(Journal $journal) { $this->beConstructedWith($journal); } - function it_is_initializable() + public function it_is_initializable() { $this->beAnInstanceOf('Http\Client\Common\Plugin\JournalPlugin'); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_records_success(Journal $journal, RequestInterface $request, ResponseInterface $response) + public function it_records_success(Journal $journal, RequestInterface $request, ResponseInterface $response) { - $next = function (RequestInterface $receivedRequest) use($request, $response) { + $next = function (RequestInterface $receivedRequest) use ($request, $response) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($response->getWrappedObject()); } @@ -41,10 +42,10 @@ function it_records_success(Journal $journal, RequestInterface $request, Respons $this->handleRequest($request, $next, function () {}); } - function it_records_failure(Journal $journal, RequestInterface $request) + public function it_records_failure(Journal $journal, RequestInterface $request) { $exception = new TransferException(); - $next = function (RequestInterface $receivedRequest) use($request, $exception) { + $next = function (RequestInterface $receivedRequest) use ($request, $exception) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpRejectedPromise($exception); } diff --git a/spec/Plugin/PluginStub.php b/spec/Plugin/PluginStub.php new file mode 100644 index 0000000..ead2a57 --- /dev/null +++ b/spec/Plugin/PluginStub.php @@ -0,0 +1,25 @@ +shouldHaveType('Http\Client\Common\Plugin\QueryDefaultsPlugin'); + $this->shouldHaveType(QueryDefaultsPlugin::class); } public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } public function it_sets_the_default_header(RequestInterface $request, UriInterface $uri) @@ -35,9 +36,7 @@ public function it_sets_the_default_header(RequestInterface $request, UriInterfa $uri->withQuery('test=true&foo=bar')->shouldBeCalled()->willReturn($uri); $request->withUri($uri)->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () { - }, function () { - }); + $this->handleRequest($request, PluginStub::next(), function () {}); } public function it_does_not_replace_existing_request_value(RequestInterface $request, UriInterface $uri) @@ -52,8 +51,6 @@ public function it_does_not_replace_existing_request_value(RequestInterface $req $uri->withQuery('foo=new&bar=barDefault')->shouldBeCalled()->willReturn($uri); $request->withUri($uri)->shouldBeCalled()->willReturn($request); - $this->handleRequest($request, function () { - }, function () { - }); + $this->handleRequest($request, PluginStub::next(), function () {}); } } diff --git a/spec/Plugin/RedirectPluginSpec.php b/spec/Plugin/RedirectPluginSpec.php index 97197e1..9bcfb7f 100644 --- a/spec/Plugin/RedirectPluginSpec.php +++ b/spec/Plugin/RedirectPluginSpec.php @@ -2,28 +2,33 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Exception\CircularRedirectionException; +use Http\Client\Common\Exception\MultipleRedirectionException; +use Http\Client\Common\Plugin; use Http\Client\Common\Plugin\RedirectPlugin; +use Http\Client\Exception\HttpException; use Http\Client\Promise\HttpFulfilledPromise; +use Http\Client\Promise\HttpRejectedPromise; use Http\Promise\Promise; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; -use PhpSpec\ObjectBehavior; -use Prophecy\Argument; class RedirectPluginSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\RedirectPlugin'); + $this->shouldHaveType(RedirectPlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_redirects_on_302( + public function it_redirects_on_302( UriInterface $uri, UriInterface $uriRedirect, RequestInterface $request, @@ -32,7 +37,8 @@ function it_redirects_on_302( ResponseInterface $finalResponse, Promise $promise ) { - $responseRedirect->getStatusCode()->willReturn('302'); + $this->beConstructedWith(['stream_factory' => null]); + $responseRedirect->getStatusCode()->willReturn(302); $responseRedirect->hasHeader('Location')->willReturn(true); $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); @@ -48,14 +54,13 @@ function it_redirects_on_302( $modifiedRequest->getUri()->willReturn($uriRedirect); $modifiedRequest->getMethod()->willReturn('GET'); - - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } }; - $first = function (RequestInterface $receivedRequest) use($modifiedRequest, $promise) { + $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { return $promise->getWrappedObject(); } @@ -65,55 +70,24 @@ function it_redirects_on_302( $promise->wait()->shouldBeCalled()->willReturn($finalResponse); $finalPromise = $this->handleRequest($request, $next, $first); - $finalPromise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); + $finalPromise->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); $finalPromise->wait()->shouldReturn($finalResponse); } - function it_use_storage_on_301(UriInterface $uri, UriInterface $uriRedirect, RequestInterface $request, RequestInterface $modifiedRequest) - { - $this->beAnInstanceOf('spec\Http\Client\Common\Plugin\RedirectPluginStub'); - $this->beConstructedWith($uriRedirect, '/original', '301'); - - $next = function () { - throw new \Exception('Must not be called'); - }; - - $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/original'); - $request->withUri($uriRedirect)->willReturn($modifiedRequest); - - $modifiedRequest->getUri()->willReturn($uriRedirect); - $modifiedRequest->getMethod()->willReturn('GET'); - - $uriRedirect->__toString()->willReturn('/redirect'); - - $this->handleRequest($request, $next, function () {}); - } - - function it_stores_a_301( + public function it_use_storage_on_301( UriInterface $uri, UriInterface $uriRedirect, RequestInterface $request, - ResponseInterface $responseRedirect, RequestInterface $modifiedRequest, ResponseInterface $finalResponse, - Promise $promise + ResponseInterface $redirectResponse ) { - - $this->beAnInstanceOf('spec\Http\Client\Common\Plugin\RedirectPluginStub'); - $this->beConstructedWith($uriRedirect, '', '301'); - + $this->beConstructedWith(['stream_factory' => null]); $request->getUri()->willReturn($uri); - $uri->__toString()->willReturn('/301-url'); - - $responseRedirect->getStatusCode()->willReturn('301'); - $responseRedirect->hasHeader('Location')->willReturn(true); - $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); - + $uri->__toString()->willReturn('/original'); $uri->withPath('/redirect')->willReturn($uriRedirect); - $uriRedirect->withFragment('')->willReturn($uriRedirect); $uriRedirect->withQuery('')->willReturn($uriRedirect); - + $uriRedirect->withFragment('')->willReturn($uriRedirect); $request->withUri($uriRedirect)->willReturn($modifiedRequest); $modifiedRequest->getUri()->willReturn($uriRedirect); @@ -121,26 +95,58 @@ function it_stores_a_301( $uriRedirect->__toString()->willReturn('/redirect'); - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } - }; + $finalResponse->getStatusCode()->willReturn(200); - $first = function (RequestInterface $receivedRequest) use($modifiedRequest, $promise) { - if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { - return $promise->getWrappedObject(); + $redirectResponse->getStatusCode()->willReturn(301); + $redirectResponse->hasHeader('Location')->willReturn(true); + $redirectResponse->getHeaderLine('Location')->willReturn('/redirect'); + + $nextCalled = false; + $next = function (RequestInterface $request) use (&$nextCalled, $finalResponse, $redirectResponse): Promise { + switch ($request->getUri()) { + case '/original': + if ($nextCalled) { + throw new \Exception('Must only be called once'); + } + $nextCalled = true; + + return new HttpFulfilledPromise($redirectResponse->getWrappedObject()); + case '/redirect': + + return new HttpFulfilledPromise($finalResponse->getWrappedObject()); + default: + throw new \Exception('Test setup error with request uri '.$request->getUri()); } }; + $first = $this->buildFirst($modifiedRequest, $next); - $promise->getState()->willReturn(Promise::FULFILLED); - $promise->wait()->shouldBeCalled()->willReturn($finalResponse); + $this->handleRequest($request, $next, $first); + // rebuild first as this is expected to be called again + $first = $this->buildFirst($modifiedRequest, $next); + // next should not be called again $this->handleRequest($request, $next, $first); - $this->hasStorage('/301-url')->shouldReturn(true); } - function it_replace_full_url( + private function buildFirst(RequestInterface $modifiedRequest, callable $next): callable + { + $redirectPlugin = $this; + $firstCalled = false; + + return function (RequestInterface $request) use (&$modifiedRequest, $redirectPlugin, $next, &$firstCalled) { + if ($firstCalled) { + throw new \Exception('Only one restart expected'); + } + $firstCalled = true; + if ($modifiedRequest->getWrappedObject() !== $request) { + //throw new \Exception('Redirection failed'); + } + + return $redirectPlugin->getWrappedObject()->handleRequest($request, $next, $this); + }; + } + + public function it_replace_full_url( UriInterface $uri, UriInterface $uriRedirect, RequestInterface $request, @@ -149,15 +155,19 @@ function it_replace_full_url( ResponseInterface $finalResponse, Promise $promise ) { + $this->beConstructedWith(['stream_factory' => null]); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('/original'); - $responseRedirect->getStatusCode()->willReturn('302'); + $responseRedirect->getStatusCode()->willReturn(302); $responseRedirect->hasHeader('Location')->willReturn(true); $responseRedirect->getHeaderLine('Location')->willReturn('https://server.com:8000/redirect?query#fragment'); $request->getUri()->willReturn($uri); $uri->withScheme('https')->willReturn($uriRedirect); + $uri->withPath('/redirect')->willReturn($uri); + $uri->withQuery('query')->willReturn($uri); + $uri->withFragment('fragment')->willReturn($uri); $uriRedirect->withHost('server.com')->willReturn($uriRedirect); $uriRedirect->withPort('8000')->willReturn($uriRedirect); $uriRedirect->withPath('/redirect')->willReturn($uriRedirect); @@ -171,13 +181,13 @@ function it_replace_full_url( $uriRedirect->__toString()->willReturn('/redirect'); - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } }; - $first = function (RequestInterface $receivedRequest) use($modifiedRequest, $promise) { + $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { return $promise->getWrappedObject(); } @@ -189,9 +199,9 @@ function it_replace_full_url( $this->handleRequest($request, $next, $first); } - function it_throws_http_exception_on_no_location(RequestInterface $request, UriInterface $uri, ResponseInterface $responseRedirect) + public function it_throws_http_exception_on_no_location(RequestInterface $request, UriInterface $uri, ResponseInterface $responseRedirect) { - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } @@ -199,17 +209,17 @@ function it_throws_http_exception_on_no_location(RequestInterface $request, UriI $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('/original'); - $responseRedirect->getStatusCode()->willReturn('302'); + $responseRedirect->getStatusCode()->willReturn(302); $responseRedirect->hasHeader('Location')->willReturn(false); $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow('Http\Client\Exception\HttpException')->duringWait(); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(HttpException::class)->duringWait(); } - function it_throws_http_exception_on_invalid_location(RequestInterface $request, UriInterface $uri, ResponseInterface $responseRedirect) + public function it_throws_http_exception_on_invalid_location(RequestInterface $request, UriInterface $uri, ResponseInterface $responseRedirect) { - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } @@ -219,47 +229,47 @@ function it_throws_http_exception_on_invalid_location(RequestInterface $request, $uri->__toString()->willReturn('/original'); $responseRedirect->getHeaderLine('Location')->willReturn('scheme:///invalid'); - $responseRedirect->getStatusCode()->willReturn('302'); + $responseRedirect->getStatusCode()->willReturn(302); $responseRedirect->hasHeader('Location')->willReturn(true); $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow('Http\Client\Exception\HttpException')->duringWait(); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(HttpException::class)->duringWait(); } - function it_throw_multi_redirect_exception_on_300(RequestInterface $request, ResponseInterface $responseRedirect) + public function it_throw_multi_redirect_exception_on_300(RequestInterface $request, ResponseInterface $responseRedirect) { - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } }; $this->beConstructedWith(['preserve_header' => true, 'use_default_for_multiple' => false]); - $responseRedirect->getStatusCode()->willReturn('300'); + $responseRedirect->getStatusCode()->willReturn(300); $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow('Http\Client\Common\Exception\MultipleRedirectionException')->duringWait(); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(MultipleRedirectionException::class)->duringWait(); } - function it_throw_multi_redirect_exception_on_300_if_no_location(RequestInterface $request, ResponseInterface $responseRedirect) + public function it_throw_multi_redirect_exception_on_300_if_no_location(RequestInterface $request, ResponseInterface $responseRedirect) { - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } }; - $responseRedirect->getStatusCode()->willReturn('300'); + $responseRedirect->getStatusCode()->willReturn(300); $responseRedirect->hasHeader('Location')->willReturn(false); $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow('Http\Client\Common\Exception\MultipleRedirectionException')->duringWait(); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(MultipleRedirectionException::class)->duringWait(); } - function it_switch_method_for_302( + public function it_switch_method_for_302( UriInterface $uri, UriInterface $uriRedirect, RequestInterface $request, @@ -268,10 +278,11 @@ function it_switch_method_for_302( ResponseInterface $finalResponse, Promise $promise ) { + $this->beConstructedWith(['stream_factory' => null]); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('/original'); - $responseRedirect->getStatusCode()->willReturn('302'); + $responseRedirect->getStatusCode()->willReturn(302); $responseRedirect->hasHeader('Location')->willReturn(true); $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); @@ -281,20 +292,18 @@ function it_switch_method_for_302( $uriRedirect->withQuery('')->willReturn($uriRedirect); $request->withUri($uriRedirect)->willReturn($modifiedRequest); - $modifiedRequest->getUri()->willReturn($uriRedirect); - $modifiedRequest->getUri()->willReturn($uriRedirect); $uriRedirect->__toString()->willReturn('/redirect'); $modifiedRequest->getMethod()->willReturn('POST'); - $modifiedRequest->withMethod('GET')->willReturn($modifiedRequest); + $modifiedRequest->withMethod('GET')->shouldBeCalled()->willReturn($modifiedRequest); - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } }; - $first = function (RequestInterface $receivedRequest) use($modifiedRequest, $promise) { + $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { return $promise->getWrappedObject(); } @@ -306,7 +315,7 @@ function it_switch_method_for_302( $this->handleRequest($request, $next, $first); } - function it_clears_headers( + public function it_does_not_switch_method_for_302_with_strict_option( UriInterface $uri, UriInterface $uriRedirect, RequestInterface $request, @@ -315,12 +324,12 @@ function it_clears_headers( ResponseInterface $finalResponse, Promise $promise ) { - $this->beConstructedWith(['preserve_header' => ['Accept']]); + $this->beConstructedWith(['strict' => true]); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('/original'); - $responseRedirect->getStatusCode()->willReturn('302'); + $responseRedirect->getStatusCode()->willReturn(302); $responseRedirect->hasHeader('Location')->willReturn(true); $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); @@ -330,21 +339,18 @@ function it_clears_headers( $uriRedirect->withQuery('')->willReturn($uriRedirect); $request->withUri($uriRedirect)->willReturn($modifiedRequest); - $modifiedRequest->getUri()->willReturn($uriRedirect); $uriRedirect->__toString()->willReturn('/redirect'); - $modifiedRequest->getMethod()->willReturn('GET'); - $modifiedRequest->getHeaders()->willReturn(['Accept' => 'value', 'Cookie' => 'value']); - $modifiedRequest->withoutHeader('Cookie')->willReturn($modifiedRequest); - $modifiedRequest->getUri()->willReturn($uriRedirect); + $modifiedRequest->getMethod()->willReturn('POST'); + $modifiedRequest->withMethod('GET')->shouldNotBeCalled(); - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } }; - $first = function (RequestInterface $receivedRequest) use($modifiedRequest, $promise) { + $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { return $promise->getWrappedObject(); } @@ -356,114 +362,155 @@ function it_clears_headers( $this->handleRequest($request, $next, $first); } - function it_throws_circular_redirection_exception(UriInterface $uri, UriInterface $uriRedirect, RequestInterface $request, ResponseInterface $responseRedirect, RequestInterface $modifiedRequest) - { - $first = function() {}; - - $this->beAnInstanceOf('spec\Http\Client\Common\Plugin\RedirectPluginStubCircular'); - $this->beConstructedWith(spl_object_hash((object)$first)); + public function it_clears_headers( + UriInterface $uri, + UriInterface $uriRedirect, + RequestInterface $request, + ResponseInterface $responseRedirect, + RequestInterface $modifiedRequest, + ResponseInterface $finalResponse, + Promise $promise + ) { + $this->beConstructedWith([ + 'preserve_header' => ['Accept'], + 'stream_factory' => null, + ]); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('/original'); - $responseRedirect->getStatusCode()->willReturn('302'); + $responseRedirect->getStatusCode()->willReturn(302); $responseRedirect->hasHeader('Location')->willReturn(true); $responseRedirect->getHeaderLine('Location')->willReturn('/redirect'); + $request->getUri()->willReturn($uri); $uri->withPath('/redirect')->willReturn($uriRedirect); $uriRedirect->withFragment('')->willReturn($uriRedirect); $uriRedirect->withQuery('')->willReturn($uriRedirect); $request->withUri($uriRedirect)->willReturn($modifiedRequest); + $modifiedRequest->getUri()->willReturn($uriRedirect); $uriRedirect->__toString()->willReturn('/redirect'); $modifiedRequest->getMethod()->willReturn('GET'); + $modifiedRequest->getHeaders()->willReturn(['Accept' => 'value', 'Cookie' => 'value']); + $modifiedRequest->withoutHeader('Cookie')->willReturn($modifiedRequest); + $modifiedRequest->getUri()->willReturn($uriRedirect); - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { + $next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); } }; - $promise = $this->handleRequest($request, $next, $first); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); - $promise->shouldThrow('Http\Client\Common\Exception\CircularRedirectionException')->duringWait(); + $first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) { + if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { + return $promise->getWrappedObject(); + } + }; + + $promise->getState()->willReturn(Promise::FULFILLED); + $promise->wait()->shouldBeCalled()->willReturn($finalResponse); + + $this->handleRequest($request, $next, $first); } - function it_redirects_http_to_https( - UriInterface $uri, - UriInterface $uriRedirect, + /** + * This is the "redirection does not redirect case. + */ + public function it_throws_circular_redirection_exception_on_redirect_that_does_not_change_url( + UriInterface $redirectUri, RequestInterface $request, - ResponseInterface $responseRedirect, - RequestInterface $modifiedRequest, - ResponseInterface $finalResponse, - Promise $promise + ResponseInterface $redirectResponse ) { - $responseRedirect->getStatusCode()->willReturn('302'); - $responseRedirect->hasHeader('Location')->willReturn(true); - $responseRedirect->getHeaderLine('Location')->willReturn('https://my-site.com/original'); + $redirectResponse->getStatusCode()->willReturn(302); + $redirectResponse->hasHeader('Location')->willReturn(true); + $redirectResponse->getHeaderLine('Location')->willReturn('/redirect'); - $request->getUri()->willReturn($uri); - $request->withUri($uriRedirect)->willReturn($modifiedRequest); - $uri->__toString()->willReturn('http://my-site.com/original'); + $next = function () use ($redirectResponse): Promise { + return new HttpFulfilledPromise($redirectResponse->getWrappedObject()); + }; - $uri->withScheme('https')->willReturn($uriRedirect); - $uriRedirect->withHost('my-site.com')->willReturn($uriRedirect); - $uriRedirect->withPath('/original')->willReturn($uriRedirect); - $uriRedirect->withFragment('')->willReturn($uriRedirect); - $uriRedirect->withQuery('')->willReturn($uriRedirect); - $uriRedirect->__toString()->willReturn('https://my-site.com/original'); + $first = function () { + throw new \Exception('First should never be called'); + }; - $modifiedRequest->getUri()->willReturn($uriRedirect); - $modifiedRequest->getMethod()->willReturn('GET'); + $request->getUri()->willReturn($redirectUri); + $redirectUri->__toString()->willReturn('/redirect'); - $next = function (RequestInterface $receivedRequest) use($request, $responseRedirect) { - if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - return new HttpFulfilledPromise($responseRedirect->getWrappedObject()); - } + $redirectUri->withPath('/redirect')->willReturn($redirectUri); + $redirectUri->withFragment('')->willReturn($redirectUri); + $redirectUri->withQuery('')->willReturn($redirectUri); + + $request->withUri($redirectUri)->willReturn($request); + $redirectUri->__toString()->willReturn('/redirect'); + $request->getMethod()->willReturn('GET'); + + $promise = $this->handleRequest($request, $next, $first); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(CircularRedirectionException::class)->duringWait(); + } + + /** + * This is a redirection flipping back and forth between two paths. + * + * There could be a larger loop but the logic in the plugin stays the same with as many redirects as needed. + */ + public function it_throws_circular_redirection_exception_on_alternating_redirect( + UriInterface $uri, + UriInterface $redirectUri, + RequestInterface $request, + ResponseInterface $redirectResponse1, + ResponseInterface $redirectResponse2, + RequestInterface $modifiedRequest + ) { + $redirectResponse1->getStatusCode()->willReturn(302); + $redirectResponse1->hasHeader('Location')->willReturn(true); + $redirectResponse1->getHeaderLine('Location')->willReturn('/redirect'); + + $redirectResponse2->getStatusCode()->willReturn(302); + $redirectResponse2->hasHeader('Location')->willReturn(true); + $redirectResponse2->getHeaderLine('Location')->willReturn('/original'); + + $next = function (RequestInterface $currentRequest) use ($request, $redirectResponse1, $redirectResponse2): Promise { + return ($currentRequest === $request->getWrappedObject()) + ? new HttpFulfilledPromise($redirectResponse1->getWrappedObject()) + : new HttpFulfilledPromise($redirectResponse2->getWrappedObject()) + ; }; - $first = function (RequestInterface $receivedRequest) use($modifiedRequest, $promise) { - if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) { - return $promise->getWrappedObject(); + $redirectPlugin = $this; + $firstCalled = false; + $first = function (RequestInterface $request) use (&$firstCalled, $redirectPlugin, $next, &$first) { + if ($firstCalled) { + throw new \Exception('only one redirect expected'); } + $firstCalled = true; + + return $redirectPlugin->getWrappedObject()->handleRequest($request, $next, $first); }; - $promise->getState()->willReturn(Promise::FULFILLED); - $promise->wait()->shouldBeCalled()->willReturn($finalResponse); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('/original'); - $finalPromise = $this->handleRequest($request, $next, $first); - $finalPromise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); - $finalPromise->wait()->shouldReturn($finalResponse); - } -} + $modifiedRequest->getUri()->willReturn($redirectUri); + $redirectUri->__toString()->willReturn('/redirect'); -class RedirectPluginStub extends RedirectPlugin -{ - public function __construct(UriInterface $uri, $storedUrl, $status, array $config = []) - { - parent::__construct($config); + $uri->withPath('/redirect')->willReturn($redirectUri); + $redirectUri->withFragment('')->willReturn($redirectUri); + $redirectUri->withQuery('')->willReturn($redirectUri); - $this->redirectStorage[$storedUrl] = [ - 'uri' => $uri, - 'status' => $status - ]; - } + $redirectUri->withPath('/original')->willReturn($uri); + $uri->withFragment('')->willReturn($uri); + $uri->withQuery('')->willReturn($uri); - public function hasStorage($url) - { - return isset($this->redirectStorage[$url]); - } -} + $request->withUri($redirectUri)->willReturn($modifiedRequest); + $request->getMethod()->willReturn('GET'); + $modifiedRequest->withUri($uri)->willReturn($request); + $modifiedRequest->getMethod()->willReturn('GET'); -class RedirectPluginStubCircular extends RedirectPlugin -{ - public function __construct($chainHash) - { - $this->circularDetection = [ - $chainHash => [ - '/redirect' - ] - ]; + $promise = $this->handleRequest($request, $next, $first); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow(CircularRedirectionException::class)->duringWait(); } } diff --git a/spec/Plugin/RequestMatcherPluginSpec.php b/spec/Plugin/RequestMatcherPluginSpec.php index 4fe9aea..246dc29 100644 --- a/spec/Plugin/RequestMatcherPluginSpec.php +++ b/spec/Plugin/RequestMatcherPluginSpec.php @@ -3,30 +3,31 @@ namespace spec\Http\Client\Common\Plugin; use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\RequestMatcherPlugin; use Http\Message\RequestMatcher; use Http\Promise\Promise; -use Psr\Http\Message\RequestInterface; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; class RequestMatcherPluginSpec extends ObjectBehavior { - function let(RequestMatcher $requestMatcher, Plugin $plugin) + public function let(RequestMatcher $requestMatcher, Plugin $plugin) { $this->beConstructedWith($requestMatcher, $plugin); } - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\RequestMatcherPlugin'); + $this->shouldHaveType(RequestMatcherPlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_matches_a_request_and_delegates_to_plugin( + public function it_matches_a_request_and_delegates_to_plugin( RequestInterface $request, RequestMatcher $requestMatcher, Plugin $plugin @@ -34,10 +35,10 @@ function it_matches_a_request_and_delegates_to_plugin( $requestMatcher->matches($request)->willReturn(true); $plugin->handleRequest($request, Argument::type('callable'), Argument::type('callable'))->shouldBeCalled(); - $this->handleRequest($request, function () {}, function () {}); + $this->handleRequest($request, PluginStub::next(), function () {}); } - function it_does_not_match_a_request( + public function it_does_not_match_a_request( RequestInterface $request, RequestMatcher $requestMatcher, Plugin $plugin, @@ -46,7 +47,7 @@ function it_does_not_match_a_request( $requestMatcher->matches($request)->willReturn(false); $plugin->handleRequest($request, Argument::type('callable'), Argument::type('callable'))->shouldNotBeCalled(); - $next = function (RequestInterface $request) use($promise) { + $next = function (RequestInterface $request) use ($promise) { return $promise->getWrappedObject(); }; diff --git a/spec/Plugin/RequestSeekableBodyPluginSpec.php b/spec/Plugin/RequestSeekableBodyPluginSpec.php new file mode 100644 index 0000000..fbb5530 --- /dev/null +++ b/spec/Plugin/RequestSeekableBodyPluginSpec.php @@ -0,0 +1,46 @@ +shouldHaveType(RequestSeekableBodyPlugin::class); + } + + public function it_is_a_plugin() + { + $this->shouldImplement(Plugin::class); + } + + public function it_decorate_request_body_if_not_seekable(RequestInterface $request, StreamInterface $requestStream) + { + $request->getBody()->shouldBeCalled()->willReturn($requestStream); + $requestStream->isSeekable()->shouldBeCalled()->willReturn(false); + $requestStream->getSize()->willReturn(null); + + $request->withBody(Argument::type(BufferedStream::class))->shouldBeCalled()->willReturn($request); + + $this->handleRequest($request, PluginStub::next(), function () {}); + } + + public function it_does_not_decorate_request_body_if_seekable(RequestInterface $request, StreamInterface $requestStream) + { + $request->getBody()->shouldBeCalled()->willReturn($requestStream); + $requestStream->isSeekable()->shouldBeCalled()->willReturn(true); + $requestStream->getSize()->willReturn(null); + + $request->withBody(Argument::type(BufferedStream::class))->shouldNotBeCalled(); + + $this->handleRequest($request, PluginStub::next(), function () {}); + } +} diff --git a/spec/Plugin/ResponseSeekableBodyPluginSpec.php b/spec/Plugin/ResponseSeekableBodyPluginSpec.php new file mode 100644 index 0000000..4f75027 --- /dev/null +++ b/spec/Plugin/ResponseSeekableBodyPluginSpec.php @@ -0,0 +1,56 @@ +shouldHaveType(ResponseSeekableBodyPlugin::class); + } + + public function it_is_a_plugin() + { + $this->shouldImplement(Plugin::class); + } + + public function it_decorate_response_body_if_not_seekable(ResponseInterface $response, StreamInterface $responseStream) + { + $next = function () use ($response) { + return new HttpFulfilledPromise($response->getWrappedObject()); + }; + + $response->getBody()->shouldBeCalled()->willReturn($responseStream); + $responseStream->isSeekable()->shouldBeCalled()->willReturn(false); + $responseStream->getSize()->willReturn(null); + + $response->withBody(Argument::type(BufferedStream::class))->shouldBeCalled()->willReturn($response); + + $this->handleRequest(new Request('GET', '/'), $next, function () {}); + } + + public function it_does_not_decorate_response_body_if_seekable(ResponseInterface $response, StreamInterface $responseStream) + { + $next = function () use ($response) { + return new HttpFulfilledPromise($response->getWrappedObject()); + }; + + $response->getBody()->shouldBeCalled()->willReturn($responseStream); + $responseStream->isSeekable()->shouldBeCalled()->willReturn(true); + $responseStream->getSize()->willReturn(null); + + $response->withBody(Argument::type(BufferedStream::class))->shouldNotBeCalled(); + + $this->handleRequest(new Request('GET', '/'), $next, function () {}); + } +} diff --git a/spec/Plugin/RetryPluginSpec.php b/spec/Plugin/RetryPluginSpec.php index 4a985f3..4b749a7 100644 --- a/spec/Plugin/RetryPluginSpec.php +++ b/spec/Plugin/RetryPluginSpec.php @@ -2,95 +2,119 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\RetryPlugin; use Http\Client\Exception; use Http\Client\Promise\HttpFulfilledPromise; use Http\Client\Promise\HttpRejectedPromise; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; class RetryPluginSpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\RetryPlugin'); + $this->shouldHaveType(RetryPlugin::class); } - function it_is_a_plugin() + public function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } - function it_returns_response(RequestInterface $request, ResponseInterface $response) + public function it_returns_response(RequestInterface $request, ResponseInterface $response) { - $next = function (RequestInterface $receivedRequest) use($request, $response) { + $next = function (RequestInterface $receivedRequest) use ($request, $response) { if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { return new HttpFulfilledPromise($response->getWrappedObject()); } }; - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); + $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); } - function it_throws_exception_on_multiple_exceptions(RequestInterface $request) + public function it_throws_exception_on_multiple_exceptions(RequestInterface $request) { $exception1 = new Exception\NetworkException('Exception 1', $request->getWrappedObject()); $exception2 = new Exception\NetworkException('Exception 2', $request->getWrappedObject()); $count = 0; - $next = function (RequestInterface $receivedRequest) use($request, $exception1, $exception2, &$count) { - $count++; + $next = function (RequestInterface $receivedRequest) use ($request, $exception1, $exception2, &$count) { + ++$count; if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - if ($count == 1) { + if (1 == $count) { return new HttpRejectedPromise($exception1); } - if ($count == 2) { + if (2 == $count) { return new HttpRejectedPromise($exception2); } } }; $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpRejectedPromise'); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); $promise->shouldThrow($exception2)->duringWait(); } - function it_returns_response_on_second_try(RequestInterface $request, ResponseInterface $response) + public function it_does_not_retry_client_errors(RequestInterface $request, ResponseInterface $response) + { + $exception = new Exception\HttpException('Exception', $request->getWrappedObject(), $response->getWrappedObject()); + + $seen = false; + $next = function (RequestInterface $receivedRequest) use ($request, $exception, &$seen) { + if (!Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { + throw new \Exception('Unexpected request received'); + } + if ($seen) { + throw new \Exception('This should only be called once'); + } + $seen = true; + + return new HttpRejectedPromise($exception); + }; + + $promise = $this->handleRequest($request, $next, function () {}); + $promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class); + $promise->shouldThrow($exception)->duringWait(); + } + + public function it_returns_response_on_second_try(RequestInterface $request, ResponseInterface $response) { $exception = new Exception\NetworkException('Exception 1', $request->getWrappedObject()); $count = 0; - $next = function (RequestInterface $receivedRequest) use($request, $exception, $response, &$count) { - $count++; + $next = function (RequestInterface $receivedRequest) use ($request, $exception, $response, &$count) { + ++$count; if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - if ($count == 1) { + if (1 == $count) { return new HttpRejectedPromise($exception); } - if ($count == 2) { + if (2 == $count) { return new HttpFulfilledPromise($response->getWrappedObject()); } } }; $promise = $this->handleRequest($request, $next, function () {}); - $promise->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); + $promise->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); $promise->wait()->shouldReturn($response); } - function it_respects_custom_exception_decider(RequestInterface $request, ResponseInterface $response) + public function it_respects_custom_exception_decider(RequestInterface $request, ResponseInterface $response) { $this->beConstructedWith([ 'exception_decider' => function (RequestInterface $request, Exception $e) { return false; - } + }, ]); $exception = new Exception\NetworkException('Exception', $request->getWrappedObject()); $called = false; - $next = function (RequestInterface $receivedRequest) use($exception, &$called) { + $next = function (RequestInterface $receivedRequest) use ($exception, &$called) { if ($called) { throw new \RuntimeException('Did not expect to be called multiple times'); } @@ -104,33 +128,41 @@ function it_respects_custom_exception_decider(RequestInterface $request, Respons $promise->shouldThrow($exception)->duringWait(); } - function it_does_not_keep_history_of_old_failure(RequestInterface $request, ResponseInterface $response) + public function it_does_not_keep_history_of_old_failure(RequestInterface $request, ResponseInterface $response) { $exception = new Exception\NetworkException('Exception 1', $request->getWrappedObject()); $count = 0; - $next = function (RequestInterface $receivedRequest) use($request, $exception, $response, &$count) { - $count++; + $next = function (RequestInterface $receivedRequest) use ($request, $exception, $response, &$count) { + ++$count; if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) { - if ($count % 2 == 1) { + if (1 == $count % 2) { return new HttpRejectedPromise($exception); } - if ($count % 2 == 0) { + if (0 == $count % 2) { return new HttpFulfilledPromise($response->getWrappedObject()); } } }; - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); - $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf('Http\Client\Promise\HttpFulfilledPromise'); + $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); + $this->handleRequest($request, $next, function () {})->shouldReturnAnInstanceOf(HttpFulfilledPromise::class); + } + + public function it_has_an_exponential_default_error_response_delay(RequestInterface $request, ResponseInterface $response) + { + $this->defaultErrorResponseDelay($request, $response, 0)->shouldBe(500000); + $this->defaultErrorResponseDelay($request, $response, 1)->shouldBe(1000000); + $this->defaultErrorResponseDelay($request, $response, 2)->shouldBe(2000000); + $this->defaultErrorResponseDelay($request, $response, 3)->shouldBe(4000000); } - function it_has_an_exponential_default_delay(RequestInterface $request, Exception\HttpException $exception) + public function it_has_an_exponential_default_exception_delay(RequestInterface $request, Exception\HttpException $exception) { - $this->defaultDelay($request, $exception, 0)->shouldBe(500000); - $this->defaultDelay($request, $exception, 1)->shouldBe(1000000); - $this->defaultDelay($request, $exception, 2)->shouldBe(2000000); - $this->defaultDelay($request, $exception, 3)->shouldBe(4000000); + $this->defaultExceptionDelay($request, $exception, 0)->shouldBe(500000); + $this->defaultExceptionDelay($request, $exception, 1)->shouldBe(1000000); + $this->defaultExceptionDelay($request, $exception, 2)->shouldBe(2000000); + $this->defaultExceptionDelay($request, $exception, 3)->shouldBe(4000000); } } diff --git a/spec/PluginClientFactorySpec.php b/spec/PluginClientFactorySpec.php index 1f8d9e8..6b5868d 100644 --- a/spec/PluginClientFactorySpec.php +++ b/spec/PluginClientFactorySpec.php @@ -2,24 +2,26 @@ namespace spec\Http\Client\Common; +use Http\Client\Common\PluginClient; +use Http\Client\Common\PluginClientFactory; use Http\Client\HttpClient; use PhpSpec\ObjectBehavior; class PluginClientFactorySpec extends ObjectBehavior { - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\PluginClientFactory'); + $this->shouldHaveType(PluginClientFactory::class); } - function it_returns_a_plugin_client(HttpClient $httpClient) + public function it_returns_a_plugin_client(HttpClient $httpClient) { $client = $this->createClient($httpClient); - $client->shouldHaveType('Http\Client\Common\PluginClient'); + $client->shouldHaveType(PluginClient::class); } - function it_does_not_construct_plugin_client_with_client_name_option(HttpClient $httpClient) + public function it_does_not_construct_plugin_client_with_client_name_option(HttpClient $httpClient) { $this->createClient($httpClient, [], ['client_name' => 'Default']); } diff --git a/spec/PluginClientSpec.php b/spec/PluginClientSpec.php index addb2e8..fb6b153 100644 --- a/spec/PluginClientSpec.php +++ b/spec/PluginClientSpec.php @@ -2,46 +2,47 @@ namespace spec\Http\Client\Common; +use Http\Client\Common\Exception\LoopException; +use Http\Client\Common\Plugin; +use Http\Client\Common\PluginClient; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; -use Http\Client\Common\FlexibleHttpClient; -use Http\Client\Common\Plugin; use Http\Promise\Promise; +use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use PhpSpec\ObjectBehavior; class PluginClientSpec extends ObjectBehavior { - function let(HttpClient $httpClient) + public function let(HttpClient $httpClient) { $this->beConstructedWith($httpClient); } - function it_is_initializable() + public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\PluginClient'); + $this->shouldHaveType(PluginClient::class); } - function it_is_an_http_client() + public function it_is_an_http_client() { - $this->shouldImplement('Http\Client\HttpClient'); + $this->shouldImplement(HttpClient::class); } - function it_is_an_http_async_client() + public function it_is_an_http_async_client() { - $this->shouldImplement('Http\Client\HttpAsyncClient'); + $this->shouldImplement(HttpAsyncClient::class); } - function it_sends_request_with_underlying_client(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) + public function it_sends_request_with_underlying_client(HttpClient $httpClient, RequestInterface $request, ResponseInterface $response) { $httpClient->sendRequest($request)->willReturn($response); $this->sendRequest($request)->shouldReturn($response); } - function it_sends_async_request_with_underlying_client(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) + public function it_sends_async_request_with_underlying_client(HttpAsyncClient $httpAsyncClient, RequestInterface $request, Promise $promise) { $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); @@ -49,7 +50,7 @@ function it_sends_async_request_with_underlying_client(HttpAsyncClient $httpAsyn $this->sendAsyncRequest($request)->shouldReturn($promise); } - function it_sends_async_request_if_no_send_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, ResponseInterface $response, Promise $promise) + public function it_sends_async_request_if_no_send_request(HttpAsyncClient $httpAsyncClient, RequestInterface $request, ResponseInterface $response, Promise $promise) { $this->beConstructedWith($httpAsyncClient); $httpAsyncClient->sendAsyncRequest($request)->willReturn($promise); @@ -58,10 +59,10 @@ function it_sends_async_request_if_no_send_request(HttpAsyncClient $httpAsyncCli $this->sendRequest($request)->shouldReturn($response); } - function it_prefers_send_request($client, RequestInterface $request, ResponseInterface $response) + public function it_prefers_send_request($client, RequestInterface $request, ResponseInterface $response) { - $client->implement('Http\Client\HttpClient'); - $client->implement('Http\Client\HttpAsyncClient'); + $client->implement(HttpClient::class); + $client->implement(HttpAsyncClient::class); $client->sendRequest($request)->willReturn($response); @@ -70,7 +71,7 @@ function it_prefers_send_request($client, RequestInterface $request, ResponseInt $this->sendRequest($request)->shouldReturn($response); } - function it_throws_loop_exception(HttpClient $httpClient, RequestInterface $request, Plugin $plugin) + public function it_throws_loop_exception(HttpClient $httpClient, RequestInterface $request, Plugin $plugin) { $plugin ->handleRequest( @@ -85,49 +86,6 @@ function it_throws_loop_exception(HttpClient $httpClient, RequestInterface $requ $this->beConstructedWith($httpClient, [$plugin]); - $this->shouldThrow('Http\Client\Common\Exception\LoopException')->duringSendRequest($request); - } - - function it_injects_debug_plugins(HttpClient $httpClient, ResponseInterface $response, RequestInterface $request, Plugin $plugin0, Plugin $plugin1, Plugin $debugPlugin) - { - $plugin0 - ->handleRequest( - $request, - Argument::type('callable'), - Argument::type('callable') - ) - ->shouldBeCalledTimes(1) - ->will(function ($args) { - return $args[1]($args[0]); - }) - ; - $plugin1 - ->handleRequest( - $request, - Argument::type('callable'), - Argument::type('callable') - ) - ->shouldBeCalledTimes(1) - ->will(function ($args) { - return $args[1]($args[0]); - }) - ; - - $debugPlugin - ->handleRequest( - $request, - Argument::type('callable'), - Argument::type('callable') - ) - ->shouldBeCalledTimes(3) - ->will(function ($args) { - return $args[1]($args[0]); - }) - ; - - $httpClient->sendRequest($request)->willReturn($response); - - $this->beConstructedWith($httpClient, [$plugin0, $plugin1], ['debug_plugins'=>[$debugPlugin]]); - $this->sendRequest($request); + $this->shouldThrow(LoopException::class)->duringSendRequest($request); } } diff --git a/src/BatchClient.php b/src/BatchClient.php index 1aa9950..51f13ff 100644 --- a/src/BatchClient.php +++ b/src/BatchClient.php @@ -1,70 +1,34 @@ - */ -class BatchClient implements HttpClient +final class BatchClient implements BatchClientInterface { /** - * @var HttpClient|ClientInterface + * @var ClientInterface */ private $client; - /** - * @param HttpClient|ClientInterface $client - */ - public function __construct($client) + public function __construct(ClientInterface $client) { - if (!($client instanceof HttpClient) && !($client instanceof ClientInterface)) { - throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface'); - } - $this->client = $client; } - /** - * {@inheritdoc} - */ - public function sendRequest(RequestInterface $request) - { - return $this->client->sendRequest($request); - } - - /** - * Send several requests. - * - * You may not assume that the requests are executed in a particular order. If the order matters - * for your application, use sendRequest sequentially. - * - * @param RequestInterface[] The requests to send - * - * @return BatchResult Containing one result per request - * - * @throws BatchException If one or more requests fails. The exception gives access to the - * BatchResult with a map of request to result for success, request to - * exception for failures - */ - public function sendRequests(array $requests) + public function sendRequests(array $requests): BatchResult { $batchResult = new BatchResult(); foreach ($requests as $request) { try { - $response = $this->sendRequest($request); + $response = $this->client->sendRequest($request); $batchResult = $batchResult->addResponse($request, $response); - } catch (Exception $e) { + } catch (ClientExceptionInterface $e) { $batchResult = $batchResult->addException($request, $e); } } diff --git a/src/BatchClientInterface.php b/src/BatchClientInterface.php new file mode 100644 index 0000000..d42eb2d --- /dev/null +++ b/src/BatchClientInterface.php @@ -0,0 +1,34 @@ + + */ +interface BatchClientInterface +{ + /** + * Send several requests. + * + * You may not assume that the requests are executed in a particular order. If the order matters + * for your application, use sendRequest sequentially. + * + * @param RequestInterface[] $requests The requests to send + * + * @return BatchResult Containing one result per request + * + * @throws BatchException If one or more requests fails. The exception gives access to the + * BatchResult with a map of request to result for success, request to + * exception for failures + */ + public function sendRequests(array $requests): BatchResult; +} diff --git a/src/BatchResult.php b/src/BatchResult.php index 710611d..ccaf83c 100644 --- a/src/BatchResult.php +++ b/src/BatchResult.php @@ -1,8 +1,10 @@ */ private $responses; /** - * @var \SplObjectStorage + * @var \SplObjectStorage */ private $exceptions; @@ -31,10 +33,8 @@ public function __construct() /** * Checks if there are any successful responses at all. - * - * @return bool */ - public function hasResponses() + public function hasResponses(): bool { return $this->responses->count() > 0; } @@ -44,7 +44,7 @@ public function hasResponses() * * @return ResponseInterface[] */ - public function getResponses() + public function getResponses(): array { $responses = []; @@ -57,12 +57,8 @@ public function getResponses() /** * Checks if there is a successful response for a request. - * - * @param RequestInterface $request - * - * @return bool */ - public function isSuccessful(RequestInterface $request) + public function isSuccessful(RequestInterface $request): bool { return $this->responses->contains($request); } @@ -70,13 +66,9 @@ public function isSuccessful(RequestInterface $request) /** * Returns the response for a successful request. * - * @param RequestInterface $request - * - * @return ResponseInterface - * * @throws \UnexpectedValueException If request was not part of the batch or failed */ - public function getResponseFor(RequestInterface $request) + public function getResponseFor(RequestInterface $request): ResponseInterface { try { return $this->responses[$request]; @@ -88,12 +80,9 @@ public function getResponseFor(RequestInterface $request) /** * Adds a response in an immutable way. * - * @param RequestInterface $request - * @param ResponseInterface $response - * * @return BatchResult the new BatchResult with this request-response pair added to it */ - public function addResponse(RequestInterface $request, ResponseInterface $response) + public function addResponse(RequestInterface $request, ResponseInterface $response): self { $new = clone $this; $new->responses->attach($request, $response); @@ -103,10 +92,8 @@ public function addResponse(RequestInterface $request, ResponseInterface $respon /** * Checks if there are any unsuccessful requests at all. - * - * @return bool */ - public function hasExceptions() + public function hasExceptions(): bool { return $this->exceptions->count() > 0; } @@ -114,9 +101,9 @@ public function hasExceptions() /** * Returns all exceptions for the unsuccessful requests. * - * @return Exception[] + * @return ClientExceptionInterface[] */ - public function getExceptions() + public function getExceptions(): array { $exceptions = []; @@ -129,12 +116,8 @@ public function getExceptions() /** * Checks if there is an exception for a request, meaning the request failed. - * - * @param RequestInterface $request - * - * @return bool */ - public function isFailed(RequestInterface $request) + public function isFailed(RequestInterface $request): bool { return $this->exceptions->contains($request); } @@ -142,13 +125,9 @@ public function isFailed(RequestInterface $request) /** * Returns the exception for a failed request. * - * @param RequestInterface $request - * - * @return Exception - * * @throws \UnexpectedValueException If request was not part of the batch or was successful */ - public function getExceptionFor(RequestInterface $request) + public function getExceptionFor(RequestInterface $request): ClientExceptionInterface { try { return $this->exceptions[$request]; @@ -160,12 +139,9 @@ public function getExceptionFor(RequestInterface $request) /** * Adds an exception in an immutable way. * - * @param RequestInterface $request - * @param Exception $exception - * * @return BatchResult the new BatchResult with this request-exception pair added to it */ - public function addException(RequestInterface $request, Exception $exception) + public function addException(RequestInterface $request, ClientExceptionInterface $exception): self { $new = clone $this; $new->exceptions->attach($request, $exception); diff --git a/src/Deferred.php b/src/Deferred.php index 075a30e..effabb2 100644 --- a/src/Deferred.php +++ b/src/Deferred.php @@ -1,26 +1,46 @@ onRejectedCallbacks = []; } - /** - * {@inheritdoc} - */ - public function then(callable $onFulfilled = null, callable $onRejected = null) + public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise { $deferred = new self($this->waitCallback); @@ -44,12 +61,12 @@ public function then(callable $onFulfilled = null, callable $onRejected = null) $response = $onFulfilled($response); } $deferred->resolve($response); - } catch (Exception $exception) { + } catch (ClientExceptionInterface $exception) { $deferred->reject($exception); } }; - $this->onRejectedCallbacks[] = function (Exception $exception) use ($onRejected, $deferred) { + $this->onRejectedCallbacks[] = function (ClientExceptionInterface $exception) use ($onRejected, $deferred) { try { if (null !== $onRejected) { $response = $onRejected($exception); @@ -58,7 +75,7 @@ public function then(callable $onFulfilled = null, callable $onRejected = null) return; } $deferred->reject($exception); - } catch (Exception $newException) { + } catch (ClientExceptionInterface $newException) { $deferred->reject($newException); } }; @@ -66,10 +83,7 @@ public function then(callable $onFulfilled = null, callable $onRejected = null) return $deferred; } - /** - * {@inheritdoc} - */ - public function getState() + public function getState(): string { return $this->state; } @@ -77,14 +91,14 @@ public function getState() /** * Resolve this deferred with a Response. */ - public function resolve(ResponseInterface $response) + public function resolve(ResponseInterface $response): void { - if (self::PENDING !== $this->state) { + if (Promise::PENDING !== $this->state) { return; } $this->value = $response; - $this->state = self::FULFILLED; + $this->state = Promise::FULFILLED; foreach ($this->onFulfilledCallbacks as $onFulfilledCallback) { $onFulfilledCallback($response); @@ -94,38 +108,39 @@ public function resolve(ResponseInterface $response) /** * Reject this deferred with an Exception. */ - public function reject(Exception $exception) + public function reject(ClientExceptionInterface $exception): void { - if (self::PENDING !== $this->state) { + if (Promise::PENDING !== $this->state) { return; } $this->failure = $exception; - $this->state = self::REJECTED; + $this->state = Promise::REJECTED; foreach ($this->onRejectedCallbacks as $onRejectedCallback) { $onRejectedCallback($exception); } } - /** - * {@inheritdoc} - */ public function wait($unwrap = true) { - if (self::PENDING === $this->state) { + if (Promise::PENDING === $this->state) { $callback = $this->waitCallback; $callback(); } if (!$unwrap) { - return; + return null; } - if (self::FULFILLED === $this->state) { + if (Promise::FULFILLED === $this->state) { return $this->value; } + if (null === $this->failure) { + throw new \RuntimeException('Internal Error: Promise is not fulfilled but has no exception stored'); + } + throw $this->failure; } } diff --git a/src/EmulatedHttpAsyncClient.php b/src/EmulatedHttpAsyncClient.php index 39f89cc..008f888 100644 --- a/src/EmulatedHttpAsyncClient.php +++ b/src/EmulatedHttpAsyncClient.php @@ -1,5 +1,7 @@ */ -class EmulatedHttpAsyncClient implements HttpClient, HttpAsyncClient +final class EmulatedHttpAsyncClient implements HttpClient, HttpAsyncClient { use HttpAsyncClientEmulator; use HttpClientDecorator; - /** - * @param HttpClient|ClientInterface $httpClient - */ - public function __construct($httpClient) + public function __construct(ClientInterface $httpClient) { - if (!($httpClient instanceof HttpClient) && !($httpClient instanceof ClientInterface)) { - throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface'); - } - $this->httpClient = $httpClient; } } diff --git a/src/EmulatedHttpClient.php b/src/EmulatedHttpClient.php index 01046c8..5c2d8c4 100644 --- a/src/EmulatedHttpClient.php +++ b/src/EmulatedHttpClient.php @@ -1,25 +1,22 @@ */ -class EmulatedHttpClient implements HttpClient, HttpAsyncClient +final class EmulatedHttpClient implements HttpClient, HttpAsyncClient { use HttpAsyncClientDecorator; use HttpClientEmulator; - /** - * @param HttpAsyncClient $httpAsyncClient - */ public function __construct(HttpAsyncClient $httpAsyncClient) { $this->httpAsyncClient = $httpAsyncClient; diff --git a/src/Exception/BatchException.php b/src/Exception/BatchException.php index 66a9271..a9cb08c 100644 --- a/src/Exception/BatchException.php +++ b/src/Exception/BatchException.php @@ -1,9 +1,11 @@ result = $result; + parent::__construct(); } /** * Returns the BatchResult that contains all responses and exceptions. - * - * @return BatchResult */ - public function getResult() + public function getResult(): BatchResult { return $this->result; } diff --git a/src/Exception/CircularRedirectionException.php b/src/Exception/CircularRedirectionException.php index 73ec521..9db927c 100644 --- a/src/Exception/CircularRedirectionException.php +++ b/src/Exception/CircularRedirectionException.php @@ -1,5 +1,7 @@ */ -class CircularRedirectionException extends HttpException +final class CircularRedirectionException extends HttpException { } diff --git a/src/Exception/ClientErrorException.php b/src/Exception/ClientErrorException.php index b1f6cc8..c657a3f 100644 --- a/src/Exception/ClientErrorException.php +++ b/src/Exception/ClientErrorException.php @@ -1,5 +1,7 @@ */ -class ClientErrorException extends HttpException +final class ClientErrorException extends HttpException { } diff --git a/src/Exception/HttpClientNoMatchException.php b/src/Exception/HttpClientNoMatchException.php new file mode 100644 index 0000000..83037d3 --- /dev/null +++ b/src/Exception/HttpClientNoMatchException.php @@ -0,0 +1,33 @@ + + */ +final class HttpClientNoMatchException extends TransferException +{ + /** + * @var RequestInterface + */ + private $request; + + public function __construct(string $message, RequestInterface $request, ?\Exception $previous = null) + { + $this->request = $request; + + parent::__construct($message, 0, $previous); + } + + public function getRequest(): RequestInterface + { + return $this->request; + } +} diff --git a/src/Exception/HttpClientNotFoundException.php b/src/Exception/HttpClientNotFoundException.php index 5d33f98..509daa5 100644 --- a/src/Exception/HttpClientNotFoundException.php +++ b/src/Exception/HttpClientNotFoundException.php @@ -1,5 +1,7 @@ */ -class HttpClientNotFoundException extends TransferException +final class HttpClientNotFoundException extends TransferException { } diff --git a/src/Exception/LoopException.php b/src/Exception/LoopException.php index e834124..f4e173f 100644 --- a/src/Exception/LoopException.php +++ b/src/Exception/LoopException.php @@ -1,5 +1,7 @@ */ -class LoopException extends RequestException +final class LoopException extends RequestException { } diff --git a/src/Exception/MultipleRedirectionException.php b/src/Exception/MultipleRedirectionException.php index ae514cd..bf6c9f7 100644 --- a/src/Exception/MultipleRedirectionException.php +++ b/src/Exception/MultipleRedirectionException.php @@ -1,5 +1,7 @@ */ -class MultipleRedirectionException extends HttpException +final class MultipleRedirectionException extends HttpException { } diff --git a/src/Exception/ServerErrorException.php b/src/Exception/ServerErrorException.php index 665d724..774b97f 100644 --- a/src/Exception/ServerErrorException.php +++ b/src/Exception/ServerErrorException.php @@ -1,5 +1,7 @@ */ -class ServerErrorException extends HttpException +final class ServerErrorException extends HttpException { } diff --git a/src/FlexibleHttpClient.php b/src/FlexibleHttpClient.php index d0a6e88..c1e327f 100644 --- a/src/FlexibleHttpClient.php +++ b/src/FlexibleHttpClient.php @@ -1,5 +1,7 @@ httpClient = $client; - $this->httpAsyncClient = $client; - - if (!($this->httpClient instanceof HttpClient) && !($client instanceof ClientInterface)) { - $this->httpClient = new EmulatedHttpClient($this->httpClient); - } - - if (!($this->httpAsyncClient instanceof HttpAsyncClient)) { - $this->httpAsyncClient = new EmulatedHttpAsyncClient($this->httpAsyncClient); - } + $this->httpClient = $client instanceof ClientInterface ? $client : new EmulatedHttpClient($client); + $this->httpAsyncClient = $client instanceof HttpAsyncClient ? $client : new EmulatedHttpAsyncClient($client); } } diff --git a/src/HttpAsyncClientDecorator.php b/src/HttpAsyncClientDecorator.php index 6eb576c..91ff5af 100644 --- a/src/HttpAsyncClientDecorator.php +++ b/src/HttpAsyncClientDecorator.php @@ -1,5 +1,7 @@ httpClient->sendRequest($request); } diff --git a/src/HttpClientEmulator.php b/src/HttpClientEmulator.php index dbec1ab..7c40b50 100644 --- a/src/HttpClientEmulator.php +++ b/src/HttpClientEmulator.php @@ -1,8 +1,11 @@ sendAsyncRequest($request); @@ -24,8 +25,6 @@ public function sendRequest(RequestInterface $request) } /** - * {@inheritdoc} - * * @see HttpAsyncClient::sendAsyncRequest */ abstract public function sendAsyncRequest(RequestInterface $request); diff --git a/src/HttpClientPool.php b/src/HttpClientPool.php index c51d044..24ab421 100644 --- a/src/HttpClientPool.php +++ b/src/HttpClientPool.php @@ -1,60 +1,24 @@ clientPool[] = $client; - } - - /** - * Return an http client given a specific strategy. - * - * @throws HttpClientNotFoundException When no http client has been found into the pool - * - * @return HttpClientPoolItem Return a http client that can do both sync or async - */ - abstract protected function chooseHttpClient(); - - /** - * {@inheritdoc} - */ - public function sendAsyncRequest(RequestInterface $request) - { - return $this->chooseHttpClient()->sendAsyncRequest($request); - } - - /** - * {@inheritdoc} + * @param ClientInterface|HttpAsyncClient|HttpClientPoolItem $client */ - public function sendRequest(RequestInterface $request) - { - return $this->chooseHttpClient()->sendRequest($request); - } + public function addHttpClient($client): void; } diff --git a/src/HttpClientPool/HttpClientPool.php b/src/HttpClientPool/HttpClientPool.php new file mode 100644 index 0000000..d7c2dbd --- /dev/null +++ b/src/HttpClientPool/HttpClientPool.php @@ -0,0 +1,64 @@ +clientPool[] = $client; + } + + /** + * Return an http client given a specific strategy. + * + * @return HttpClientPoolItem Return a http client that can do both sync or async + * + * @throws HttpClientNotFoundException When no http client has been found into the pool + */ + abstract protected function chooseHttpClient(): HttpClientPoolItem; + + public function sendAsyncRequest(RequestInterface $request) + { + return $this->chooseHttpClient()->sendAsyncRequest($request); + } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->chooseHttpClient()->sendRequest($request); + } +} diff --git a/src/HttpClientPoolItem.php b/src/HttpClientPool/HttpClientPoolItem.php similarity index 58% rename from src/HttpClientPoolItem.php rename to src/HttpClientPool/HttpClientPoolItem.php index 5c79137..1bf4381 100644 --- a/src/HttpClientPoolItem.php +++ b/src/HttpClientPool/HttpClientPoolItem.php @@ -1,18 +1,29 @@ */ @@ -29,7 +40,11 @@ class HttpClientPoolItem implements HttpClient, HttpAsyncClient private $disabledAt; /** - * @var int|null Number of seconds after this client is reenable, by default null: never reenable this client + * Number of seconds until this client is enabled again after an error. + * + * null: never reenable this client. + * + * @var int|null */ private $reenableAfter; @@ -39,19 +54,22 @@ class HttpClientPoolItem implements HttpClient, HttpAsyncClient private $client; /** - * @param HttpClient|HttpAsyncClient|ClientInterface $client - * @param null|int $reenableAfter Number of seconds after this client is reenable + * @param ClientInterface|HttpAsyncClient $client + * @param int|null $reenableAfter Number of seconds until this client is enabled again after an error */ - public function __construct($client, $reenableAfter = null) + public function __construct($client, ?int $reenableAfter = null) { + if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) { + throw new \TypeError( + sprintf('%s::__construct(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + $this->client = new FlexibleHttpClient($client); $this->reenableAfter = $reenableAfter; } - /** - * {@inheritdoc} - */ - public function sendRequest(RequestInterface $request) + public function sendRequest(RequestInterface $request): ResponseInterface { if ($this->isDisabled()) { throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request); @@ -71,9 +89,6 @@ public function sendRequest(RequestInterface $request) return $response; } - /** - * {@inheritdoc} - */ public function sendAsyncRequest(RequestInterface $request) { if ($this->isDisabled()) { @@ -97,21 +112,16 @@ public function sendAsyncRequest(RequestInterface $request) /** * Whether this client is disabled or not. * - * Will also reactivate this client if possible - * - * @internal - * - * @return bool + * If the client was disabled, calling this method checks if the client can + * be reenabled and if so enables it. */ - public function isDisabled() + public function isDisabled(): bool { - $disabledAt = $this->getDisabledAt(); - - if (null !== $this->reenableAfter && null !== $disabledAt) { + if (null !== $this->reenableAfter && null !== $this->disabledAt) { // Reenable after a certain time $now = new \DateTime(); - if (($now->getTimestamp() - $disabledAt->getTimestamp()) >= $this->reenableAfter) { + if (($now->getTimestamp() - $this->disabledAt->getTimestamp()) >= $this->reenableAfter) { $this->enable(); return false; @@ -120,35 +130,21 @@ public function isDisabled() return true; } - return null !== $disabledAt; + return null !== $this->disabledAt; } /** - * Get current number of request that is send by the underlying http client. - * - * @internal - * - * @return int + * Get current number of request that are currently being sent by the underlying HTTP client. */ - public function getSendingRequestCount() + public function getSendingRequestCount(): int { return $this->sendingRequestCount; } - /** - * Return when this client has been disabled or null if it's enabled. - * - * @return \DateTime|null - */ - private function getDisabledAt() - { - return $this->disabledAt; - } - /** * Increment the request count. */ - private function incrementRequestCount() + private function incrementRequestCount(): void { ++$this->sendingRequestCount; } @@ -156,7 +152,7 @@ private function incrementRequestCount() /** * Decrement the request count. */ - private function decrementRequestCount() + private function decrementRequestCount(): void { --$this->sendingRequestCount; } @@ -164,7 +160,7 @@ private function decrementRequestCount() /** * Enable the current client. */ - private function enable() + private function enable(): void { $this->disabledAt = null; } @@ -172,7 +168,7 @@ private function enable() /** * Disable the current client. */ - private function disable() + private function disable(): void { $this->disabledAt = new \DateTime('now'); } diff --git a/src/HttpClientPool/LeastUsedClientPool.php b/src/HttpClientPool/LeastUsedClientPool.php index 6299cce..bfa1cd0 100644 --- a/src/HttpClientPool/LeastUsedClientPool.php +++ b/src/HttpClientPool/LeastUsedClientPool.php @@ -1,10 +1,10 @@ clientPool, function (HttpClientPoolItem $clientPoolItem) { return !$clientPoolItem->isDisabled(); diff --git a/src/HttpClientPool/RandomClientPool.php b/src/HttpClientPool/RandomClientPool.php index 3255f86..8ef535f 100644 --- a/src/HttpClientPool/RandomClientPool.php +++ b/src/HttpClientPool/RandomClientPool.php @@ -1,22 +1,19 @@ */ final class RandomClientPool extends HttpClientPool { - /** - * {@inheritdoc} - */ - protected function chooseHttpClient() + protected function chooseHttpClient(): HttpClientPoolItem { $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { return !$clientPoolItem->isDisabled(); diff --git a/src/HttpClientPool/RoundRobinClientPool.php b/src/HttpClientPool/RoundRobinClientPool.php index 8d8e40a..a908653 100644 --- a/src/HttpClientPool/RoundRobinClientPool.php +++ b/src/HttpClientPool/RoundRobinClientPool.php @@ -1,9 +1,10 @@ clientPool); diff --git a/src/HttpClientRouter.php b/src/HttpClientRouter.php index 8f897d2..42c6907 100644 --- a/src/HttpClientRouter.php +++ b/src/HttpClientRouter.php @@ -1,54 +1,49 @@ */ -final class HttpClientRouter implements HttpClient, HttpAsyncClient +final class HttpClientRouter implements HttpClientRouterInterface { /** - * @var array + * @var (array{matcher: RequestMatcher, client: FlexibleHttpClient})[] */ private $clients = []; - /** - * {@inheritdoc} - */ - public function sendRequest(RequestInterface $request) + public function sendRequest(RequestInterface $request): ResponseInterface { - $client = $this->chooseHttpClient($request); - - return $client->sendRequest($request); + return $this->chooseHttpClient($request)->sendRequest($request); } - /** - * {@inheritdoc} - */ public function sendAsyncRequest(RequestInterface $request) { - $client = $this->chooseHttpClient($request); - - return $client->sendAsyncRequest($request); + return $this->chooseHttpClient($request)->sendAsyncRequest($request); } /** * Add a client to the router. * - * @param HttpClient|HttpAsyncClient|ClientInterface $client - * @param RequestMatcher $requestMatcher + * @param ClientInterface|HttpAsyncClient $client */ - public function addClient($client, RequestMatcher $requestMatcher) + public function addClient($client, RequestMatcher $requestMatcher): void { + if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) { + throw new \TypeError( + sprintf('%s::addClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + $this->clients[] = [ 'matcher' => $requestMatcher, 'client' => new FlexibleHttpClient($client), @@ -57,12 +52,8 @@ public function addClient($client, RequestMatcher $requestMatcher) /** * Choose an HTTP client given a specific request. - * - * @param RequestInterface $request - * - * @return HttpClient|HttpAsyncClient|ClientInterface */ - protected function chooseHttpClient(RequestInterface $request) + private function chooseHttpClient(RequestInterface $request): FlexibleHttpClient { foreach ($this->clients as $client) { if ($client['matcher']->matches($request)) { @@ -70,6 +61,6 @@ protected function chooseHttpClient(RequestInterface $request) } } - throw new RequestException('No client found for the specified request', $request); + throw new HttpClientNoMatchException('No client found for the specified request', $request); } } diff --git a/src/HttpClientRouterInterface.php b/src/HttpClientRouterInterface.php new file mode 100644 index 0000000..ae012cf --- /dev/null +++ b/src/HttpClientRouterInterface.php @@ -0,0 +1,27 @@ + + */ +interface HttpClientRouterInterface extends HttpClient, HttpAsyncClient +{ + /** + * Add a client to the router. + * + * @param ClientInterface|HttpAsyncClient $client + */ + public function addClient($client, RequestMatcher $requestMatcher): void; +} diff --git a/src/HttpMethodsClient.php b/src/HttpMethodsClient.php index c462c10..95fee3b 100644 --- a/src/HttpMethodsClient.php +++ b/src/HttpMethodsClient.php @@ -1,209 +1,149 @@ get('/foo') - * ->post('/bar') - * ; - * - * The client also exposes the sendRequest methods of the wrapped HttpClient. - * - * @author MƔrk SƔgi-KazƔr - * @author David Buchmann - */ -class HttpMethodsClient implements HttpClient +final class HttpMethodsClient implements HttpMethodsClientInterface { /** - * @var HttpClient|ClientInterface + * @var ClientInterface */ private $httpClient; /** - * @var RequestFactory + * @var RequestFactory|RequestFactoryInterface */ private $requestFactory; /** - * @param HttpClient|ClientInterface $httpClient The client to send requests with - * @param RequestFactory $requestFactory The message factory to create requests + * @var StreamFactoryInterface|null + */ + private $streamFactory; + + /** + * @param RequestFactory|RequestFactoryInterface $requestFactory */ - public function __construct($httpClient, RequestFactory $requestFactory) + public function __construct(ClientInterface $httpClient, $requestFactory, ?StreamFactoryInterface $streamFactory = null) { - if (!($httpClient instanceof HttpClient) && !($httpClient instanceof ClientInterface)) { - throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface'); + if (!$requestFactory instanceof RequestFactory && !$requestFactory instanceof RequestFactoryInterface) { + throw new \TypeError( + sprintf('%s::__construct(): Argument #2 ($requestFactory) must be of type %s|%s, %s given', self::class, RequestFactory::class, RequestFactoryInterface::class, get_debug_type($requestFactory)) + ); + } + + if (!$requestFactory instanceof RequestFactory && null === $streamFactory) { + @trigger_error(sprintf('Passing a %s without a %s to %s::__construct() is deprecated as of version 2.3 and will be disallowed in version 3.0. A stream factory is required to create a request with a non-empty string body.', RequestFactoryInterface::class, StreamFactoryInterface::class, self::class)); } $this->httpClient = $httpClient; $this->requestFactory = $requestFactory; + $this->streamFactory = $streamFactory; } - /** - * Sends a GET request. - * - * @param string|UriInterface $uri - * @param array $headers - * - * @throws Exception - * - * @return ResponseInterface - */ - public function get($uri, array $headers = []) + public function get($uri, array $headers = []): ResponseInterface { return $this->send('GET', $uri, $headers, null); } - /** - * Sends an HEAD request. - * - * @param string|UriInterface $uri - * @param array $headers - * - * @throws Exception - * - * @return ResponseInterface - */ - public function head($uri, array $headers = []) + public function head($uri, array $headers = []): ResponseInterface { return $this->send('HEAD', $uri, $headers, null); } - /** - * Sends a TRACE request. - * - * @param string|UriInterface $uri - * @param array $headers - * - * @throws Exception - * - * @return ResponseInterface - */ - public function trace($uri, array $headers = []) + public function trace($uri, array $headers = []): ResponseInterface { return $this->send('TRACE', $uri, $headers, null); } - /** - * Sends a POST request. - * - * @param string|UriInterface $uri - * @param array $headers - * @param string|StreamInterface|null $body - * - * @throws Exception - * - * @return ResponseInterface - */ - public function post($uri, array $headers = [], $body = null) + public function post($uri, array $headers = [], $body = null): ResponseInterface { return $this->send('POST', $uri, $headers, $body); } - /** - * Sends a PUT request. - * - * @param string|UriInterface $uri - * @param array $headers - * @param string|StreamInterface|null $body - * - * @throws Exception - * - * @return ResponseInterface - */ - public function put($uri, array $headers = [], $body = null) + public function put($uri, array $headers = [], $body = null): ResponseInterface { return $this->send('PUT', $uri, $headers, $body); } - /** - * Sends a PATCH request. - * - * @param string|UriInterface $uri - * @param array $headers - * @param string|StreamInterface|null $body - * - * @throws Exception - * - * @return ResponseInterface - */ - public function patch($uri, array $headers = [], $body = null) + public function patch($uri, array $headers = [], $body = null): ResponseInterface { return $this->send('PATCH', $uri, $headers, $body); } - /** - * Sends a DELETE request. - * - * @param string|UriInterface $uri - * @param array $headers - * @param string|StreamInterface|null $body - * - * @throws Exception - * - * @return ResponseInterface - */ - public function delete($uri, array $headers = [], $body = null) + public function delete($uri, array $headers = [], $body = null): ResponseInterface { return $this->send('DELETE', $uri, $headers, $body); } - /** - * Sends an OPTIONS request. - * - * @param string|UriInterface $uri - * @param array $headers - * @param string|StreamInterface|null $body - * - * @throws Exception - * - * @return ResponseInterface - */ - public function options($uri, array $headers = [], $body = null) + public function options($uri, array $headers = [], $body = null): ResponseInterface { return $this->send('OPTIONS', $uri, $headers, $body); } + public function send(string $method, $uri, array $headers = [], $body = null): ResponseInterface + { + if (!is_string($uri) && !$uri instanceof UriInterface) { + throw new \TypeError( + sprintf('%s::send(): Argument #2 ($uri) must be of type string|%s, %s given', self::class, UriInterface::class, get_debug_type($uri)) + ); + } + + if (!is_string($body) && !$body instanceof StreamInterface && null !== $body) { + throw new \TypeError( + sprintf('%s::send(): Argument #4 ($body) must be of type string|%s|null, %s given', self::class, StreamInterface::class, get_debug_type($body)) + ); + } + + return $this->sendRequest( + self::createRequest($method, $uri, $headers, $body) + ); + } + /** - * Sends a request with any HTTP method. - * - * @param string $method HTTP method to use * @param string|UriInterface $uri - * @param array $headers * @param string|StreamInterface|null $body - * - * @throws Exception - * - * @return ResponseInterface */ - public function send($method, $uri, array $headers = [], $body = null) + private function createRequest(string $method, $uri, array $headers = [], $body = null): RequestInterface { - return $this->sendRequest($this->requestFactory->createRequest( - $method, - $uri, - $headers, - $body - )); + if ($this->requestFactory instanceof RequestFactory) { + return $this->requestFactory->createRequest( + $method, + $uri, + $headers, + $body + ); + } + + $request = $this->requestFactory->createRequest($method, $uri); + + foreach ($headers as $key => $value) { + $request = $request->withHeader($key, $value); + } + + if (null !== $body && '' !== $body) { + if (null === $this->streamFactory) { + throw new \RuntimeException('Cannot create request: A stream factory is required to create a request with a non-empty string body.'); + } + + $request = $request->withBody( + is_string($body) ? $this->streamFactory->createStream($body) : $body + ); + } + + return $request; } - /** - * Forward to the underlying HttpClient. - * - * {@inheritdoc} - */ - public function sendRequest(RequestInterface $request) + public function sendRequest(RequestInterface $request): ResponseInterface { return $this->httpClient->sendRequest($request); } diff --git a/src/HttpMethodsClientInterface.php b/src/HttpMethodsClientInterface.php new file mode 100644 index 0000000..bc0829a --- /dev/null +++ b/src/HttpMethodsClientInterface.php @@ -0,0 +1,116 @@ +get('/foo') + * ->post('/bar') + * ; + * + * The client also exposes the sendRequest methods of the wrapped HttpClient. + * + * @author MƔrk SƔgi-KazƔr + * @author David Buchmann + */ +interface HttpMethodsClientInterface extends HttpClient +{ + /** + * Sends a GET request. + * + * @param string|UriInterface $uri + * + * @throws Exception + */ + public function get($uri, array $headers = []): ResponseInterface; + + /** + * Sends an HEAD request. + * + * @param string|UriInterface $uri + * + * @throws Exception + */ + public function head($uri, array $headers = []): ResponseInterface; + + /** + * Sends a TRACE request. + * + * @param string|UriInterface $uri + * + * @throws Exception + */ + public function trace($uri, array $headers = []): ResponseInterface; + + /** + * Sends a POST request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function post($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends a PUT request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function put($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends a PATCH request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function patch($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends a DELETE request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function delete($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends an OPTIONS request. + * + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function options($uri, array $headers = [], $body = null): ResponseInterface; + + /** + * Sends a request with any HTTP method. + * + * @param string $method HTTP method to use + * @param string|UriInterface $uri + * @param string|StreamInterface|null $body + * + * @throws Exception + */ + public function send(string $method, $uri, array $headers = [], $body = null): ResponseInterface; +} diff --git a/src/Plugin.php b/src/Plugin.php index 89a2a62..99898b9 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -1,5 +1,7 @@ replace = $options['replace']; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { if ($this->replace || '' === $request->getUri()->getHost()) { $uri = $request->getUri() @@ -64,10 +63,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl return $next($request); } - /** - * @param OptionsResolver $resolver - */ - private function configureOptions(OptionsResolver $resolver) + private function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'replace' => false, diff --git a/src/Plugin/AddPathPlugin.php b/src/Plugin/AddPathPlugin.php index 1d4762e..87fcdce 100644 --- a/src/Plugin/AddPathPlugin.php +++ b/src/Plugin/AddPathPlugin.php @@ -1,8 +1,11 @@ getPath()) { @@ -69,7 +62,7 @@ public function __construct(UriInterface $uri) * * {@inheritdoc} */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { $prepend = $this->uri->getPath(); $path = $request->getUri()->getPath(); @@ -77,7 +70,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl if (substr($path, 0, strlen($prepend)) !== $prepend) { $request = $request->withUri($request->getUri() ->withPath($prepend.$path) - ); + ); } return $next($request); diff --git a/src/Plugin/AuthenticationPlugin.php b/src/Plugin/AuthenticationPlugin.php index 194712f..2336062 100644 --- a/src/Plugin/AuthenticationPlugin.php +++ b/src/Plugin/AuthenticationPlugin.php @@ -1,9 +1,12 @@ authentication = $authentication; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { $request = $this->authentication->authenticate($request); diff --git a/src/Plugin/BaseUriPlugin.php b/src/Plugin/BaseUriPlugin.php index 2c2a775..c361683 100644 --- a/src/Plugin/BaseUriPlugin.php +++ b/src/Plugin/BaseUriPlugin.php @@ -1,8 +1,11 @@ addHostPlugin->handleRequest($request, $next, $first); diff --git a/src/Plugin/ContentLengthPlugin.php b/src/Plugin/ContentLengthPlugin.php index 0f7aafa..ad69ff3 100644 --- a/src/Plugin/ContentLengthPlugin.php +++ b/src/Plugin/ContentLengthPlugin.php @@ -1,9 +1,12 @@ hasHeader('Content-Length')) { $stream = $request->getBody(); diff --git a/src/Plugin/ContentTypePlugin.php b/src/Plugin/ContentTypePlugin.php index 8ef1d62..e4844c4 100644 --- a/src/Plugin/ContentTypePlugin.php +++ b/src/Plugin/ContentTypePlugin.php @@ -1,8 +1,11 @@ sizeLimit = $options['size_limit']; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { if (!$request->hasHeader('Content-Type')) { $stream = $request->getBody(); @@ -91,13 +91,11 @@ public function handleRequest(RequestInterface $request, callable $next, callabl return $next($request); } - /** - * @param $stream StreamInterface - * - * @return bool - */ - private function isJson($stream) + private function isJson(StreamInterface $stream): bool { + if (!function_exists('json_decode')) { + return false; + } $stream->rewind(); json_decode($stream->getContents()); @@ -105,19 +103,17 @@ private function isJson($stream) return JSON_ERROR_NONE === json_last_error(); } - /** - * @param $stream StreamInterface - * - * @return \SimpleXMLElement|false - */ - private function isXml($stream) + private function isXml(StreamInterface $stream): bool { + if (!function_exists('simplexml_load_string')) { + return false; + } $stream->rewind(); $previousValue = libxml_use_internal_errors(true); $isXml = simplexml_load_string($stream->getContents()); libxml_use_internal_errors($previousValue); - return $isXml; + return false !== $isXml; } } diff --git a/src/Plugin/CookiePlugin.php b/src/Plugin/CookiePlugin.php index 156532a..23c8593 100644 --- a/src/Plugin/CookiePlugin.php +++ b/src/Plugin/CookiePlugin.php @@ -1,5 +1,7 @@ cookieJar = $cookieJar; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { $cookies = []; foreach ($this->cookieJar->getCookies() as $cookie) { @@ -91,19 +88,14 @@ public function handleRequest(RequestInterface $request, callable $next, callabl /** * Creates a cookie from a string. * - * @param RequestInterface $request - * @param $setCookie - * - * @return Cookie|null - * * @throws TransferException */ - private function createCookie(RequestInterface $request, $setCookie) + private function createCookie(RequestInterface $request, string $setCookieHeader): ?Cookie { - $parts = array_map('trim', explode(';', $setCookie)); + $parts = array_map('trim', explode(';', $setCookieHeader)); - if (empty($parts) || !strpos($parts[0], '=')) { - return; + if ('' === $parts[0] || false === strpos($parts[0], '=')) { + return null; } list($name, $cookieValue) = $this->createValueKey(array_shift($parts)); @@ -122,7 +114,7 @@ private function createCookie(RequestInterface $request, $setCookie) switch (strtolower($key)) { case 'expires': try { - $expires = CookieUtil::parseDate($value); + $expires = CookieUtil::parseDate((string) $value); } catch (UnexpectedValueException $e) { throw new TransferException( sprintf( @@ -170,15 +162,15 @@ private function createCookie(RequestInterface $request, $setCookie) /** * Separates key/value pair from cookie. * - * @param $part + * @param string $part A single cookie value in format key=value * - * @return array + * @return array{0:string, 1:string|null} */ - private function createValueKey($part) + private function createValueKey(string $part): array { $parts = explode('=', $part, 2); $key = trim($parts[0]); - $value = isset($parts[1]) ? trim($parts[1]) : true; + $value = isset($parts[1]) ? trim($parts[1]) : null; return [$key, $value]; } diff --git a/src/Plugin/DecoderPlugin.php b/src/Plugin/DecoderPlugin.php index 0239d40..3e781aa 100644 --- a/src/Plugin/DecoderPlugin.php +++ b/src/Plugin/DecoderPlugin.php @@ -1,9 +1,12 @@ useContentEncoding = $options['use_content_encoding']; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { $encodings = extension_loaded('zlib') ? ['gzip', 'deflate'] : ['identity']; @@ -65,12 +65,8 @@ public function handleRequest(RequestInterface $request, callable $next, callabl /** * Decode a response body given its Transfer-Encoding or Content-Encoding value. - * - * @param ResponseInterface $response Response to decode - * - * @return ResponseInterface New response decoded */ - private function decodeResponse(ResponseInterface $response) + private function decodeResponse(ResponseInterface $response): ResponseInterface { $response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response); @@ -83,13 +79,8 @@ private function decodeResponse(ResponseInterface $response) /** * Decode a response on a specific header (content encoding or transfer encoding mainly). - * - * @param string $headerName Name of the header - * @param ResponseInterface $response Response - * - * @return ResponseInterface A new instance of the response decoded */ - private function decodeOnEncodingHeader($headerName, ResponseInterface $response) + private function decodeOnEncodingHeader(string $headerName, ResponseInterface $response): ResponseInterface { if ($response->hasHeader($headerName)) { $encodings = $response->getHeader($headerName); @@ -120,12 +111,9 @@ private function decodeOnEncodingHeader($headerName, ResponseInterface $response /** * Decorate a stream given an encoding. * - * @param string $encoding - * @param StreamInterface $stream - * * @return StreamInterface|false A new stream interface or false if encoding is not supported */ - private function decorateStream($encoding, StreamInterface $stream) + private function decorateStream(string $encoding, StreamInterface $stream) { if ('chunked' === strtolower($encoding)) { return new Encoding\DechunkStream($stream); diff --git a/src/Plugin/ErrorPlugin.php b/src/Plugin/ErrorPlugin.php index bcc1c67..712e28a 100644 --- a/src/Plugin/ErrorPlugin.php +++ b/src/Plugin/ErrorPlugin.php @@ -1,10 +1,13 @@ */ final class ErrorPlugin implements Plugin @@ -26,10 +37,10 @@ final class ErrorPlugin implements Plugin private $onlyServerException; /** - * @param array $config { + * @param array{'only_server_exception'?: bool} $config * - * @var bool only_server_exception Whether this plugin should only throw 5XX Exceptions (default to false). - * } + * Configuration options: + * - only_server_exception: Whether this plugin should only throw 5XX Exceptions (default to false) */ public function __construct(array $config = []) { @@ -43,10 +54,7 @@ public function __construct(array $config = []) $this->onlyServerException = $options['only_server_exception']; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { $promise = $next($request); @@ -61,12 +69,12 @@ public function handleRequest(RequestInterface $request, callable $next, callabl * @param RequestInterface $request Request of the call * @param ResponseInterface $response Response of the call * + * @return ResponseInterface If status code is not in 4xx or 5xx return response + * * @throws ClientErrorException If response status code is a 4xx * @throws ServerErrorException If response status code is a 5xx - * - * @return ResponseInterface If status code is not in 4xx or 5xx return response */ - protected function transformResponseToException(RequestInterface $request, ResponseInterface $response) + private function transformResponseToException(RequestInterface $request, ResponseInterface $response): ResponseInterface { if (!$this->onlyServerException && $response->getStatusCode() >= 400 && $response->getStatusCode() < 500) { throw new ClientErrorException($response->getReasonPhrase(), $request, $response); diff --git a/src/Plugin/HeaderAppendPlugin.php b/src/Plugin/HeaderAppendPlugin.php index 26fd813..e6d6235 100644 --- a/src/Plugin/HeaderAppendPlugin.php +++ b/src/Plugin/HeaderAppendPlugin.php @@ -1,8 +1,11 @@ headers = $headers; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { foreach ($this->headers as $header => $headerValue) { $request = $request->withAddedHeader($header, $headerValue); diff --git a/src/Plugin/HeaderDefaultsPlugin.php b/src/Plugin/HeaderDefaultsPlugin.php index 6dfc111..baf8f9d 100644 --- a/src/Plugin/HeaderDefaultsPlugin.php +++ b/src/Plugin/HeaderDefaultsPlugin.php @@ -1,8 +1,11 @@ headers = $headers; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { foreach ($this->headers as $header => $headerValue) { if (!$request->hasHeader($header)) { diff --git a/src/Plugin/HeaderRemovePlugin.php b/src/Plugin/HeaderRemovePlugin.php index fc9c19d..abfb922 100644 --- a/src/Plugin/HeaderRemovePlugin.php +++ b/src/Plugin/HeaderRemovePlugin.php @@ -1,8 +1,11 @@ headers = $headers; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { foreach ($this->headers as $header) { if ($request->hasHeader($header)) { diff --git a/src/Plugin/HeaderSetPlugin.php b/src/Plugin/HeaderSetPlugin.php index 75f11d4..1ca9fb3 100644 --- a/src/Plugin/HeaderSetPlugin.php +++ b/src/Plugin/HeaderSetPlugin.php @@ -1,8 +1,11 @@ headers = $headers; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { foreach ($this->headers as $header => $headerValue) { $request = $request->withHeader($header, $headerValue); diff --git a/src/Plugin/HistoryPlugin.php b/src/Plugin/HistoryPlugin.php index 5abddbd..15597ee 100644 --- a/src/Plugin/HistoryPlugin.php +++ b/src/Plugin/HistoryPlugin.php @@ -1,9 +1,12 @@ journal = $journal; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { $journal = $this->journal; @@ -40,7 +37,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $journal->addSuccess($request, $response); return $response; - }, function (Exception $exception) use ($request, $journal) { + }, function (ClientExceptionInterface $exception) use ($request, $journal) { $journal->addFailure($request, $exception); throw $exception; diff --git a/src/Plugin/Journal.php b/src/Plugin/Journal.php index 15f3095..9faa938 100644 --- a/src/Plugin/Journal.php +++ b/src/Plugin/Journal.php @@ -1,8 +1,10 @@ queryParams = $queryParams; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { $uri = $request->getUri(); diff --git a/src/Plugin/RedirectPlugin.php b/src/Plugin/RedirectPlugin.php index d2f442e..8aebcbf 100644 --- a/src/Plugin/RedirectPlugin.php +++ b/src/Plugin/RedirectPlugin.php @@ -1,15 +1,23 @@ */ -class RedirectPlugin implements Plugin +final class RedirectPlugin implements Plugin { /** * Rule on how to redirect, change method for the new request. * * @var array */ - protected $redirectCodes = [ + private $redirectCodes = [ 300 => [ 'switch' => [ 'unless' => ['GET', 'HEAD'], @@ -78,33 +86,40 @@ class RedirectPlugin implements Plugin * false will ditch all previous headers * string[] will keep only headers with the specified names */ - protected $preserveHeader; + private $preserveHeader; /** * Store all previous redirect from 301 / 308 status code. * * @var array */ - protected $redirectStorage = []; + private $redirectStorage = []; /** * Whether the location header must be directly used for a multiple redirection status code (300). * * @var bool */ - protected $useDefaultForMultiple; + private $useDefaultForMultiple; /** - * @var array + * @var string[][] Chain identifier => list of URLs for this chain + */ + private $circularDetection = []; + + /** + * @var StreamFactoryInterface|null */ - protected $circularDetection = []; + private $streamFactory; /** - * @param array $config { + * @param array{'preserve_header'?: bool|string[], 'use_default_for_multiple'?: bool, 'strict'?: bool, 'stream_factory'?:StreamFactoryInterface} $config * - * @var bool|string[] $preserve_header True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep - * @var bool $use_default_for_multiple Whether the location header must be directly used for a multiple redirection status code (300). - * } + * Configuration options: + * - preserve_header: True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep + * - use_default_for_multiple: Whether the location header must be directly used for a multiple redirection status code (300) + * - strict: When true, redirect codes 300, 301, 302 will not modify request method and body + * - stream_factory: If set, must be a PSR-17 StreamFactoryInterface - if not set, we try to discover one */ public function __construct(array $config = []) { @@ -112,9 +127,13 @@ public function __construct(array $config = []) $resolver->setDefaults([ 'preserve_header' => true, 'use_default_for_multiple' => true, + 'strict' => false, + 'stream_factory' => null, ]); $resolver->setAllowedTypes('preserve_header', ['bool', 'array']); $resolver->setAllowedTypes('use_default_for_multiple', 'bool'); + $resolver->setAllowedTypes('strict', 'bool'); + $resolver->setAllowedTypes('stream_factory', [StreamFactoryInterface::class, 'null']); $resolver->setNormalizer('preserve_header', function (OptionsResolver $resolver, $value) { if (is_bool($value) && false === $value) { return []; @@ -122,16 +141,24 @@ public function __construct(array $config = []) return $value; }); + $resolver->setDefault('stream_factory', function (Options $options): ?StreamFactoryInterface { + return $this->guessStreamFactory(); + }); $options = $resolver->resolve($config); $this->preserveHeader = $options['preserve_header']; $this->useDefaultForMultiple = $options['use_default_for_multiple']; + + if ($options['strict']) { + $this->redirectCodes[300]['switch'] = false; + $this->redirectCodes[301]['switch'] = false; + $this->redirectCodes[302]['switch'] = false; + } + + $this->streamFactory = $options['stream_factory']; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { // Check in storage if (array_key_exists((string) $request->getUri(), $this->redirectStorage)) { @@ -142,7 +169,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl return $first($redirectRequest); } - return $next($request)->then(function (ResponseInterface $response) use ($request, $first) { + return $next($request)->then(function (ResponseInterface $response) use ($request, $first): ResponseInterface { $statusCode = $response->getStatusCode(); if (!array_key_exists($statusCode, $this->redirectCodes)) { @@ -159,7 +186,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $this->circularDetection[$chainIdentifier][] = (string) $request->getUri(); - if (in_array((string) $redirectRequest->getUri(), $this->circularDetection[$chainIdentifier])) { + if (in_array((string) $redirectRequest->getUri(), $this->circularDetection[$chainIdentifier], true)) { throw new CircularRedirectionException('Circular redirection detected', $request, $response); } @@ -170,72 +197,130 @@ public function handleRequest(RequestInterface $request, callable $next, callabl ]; } - // Call redirect request in synchrone - $redirectPromise = $first($redirectRequest); - - return $redirectPromise->wait(); + // Call redirect request synchronously + return $first($redirectRequest)->wait(); }); } /** - * Builds the redirect request. - * - * @param RequestInterface $request Original request - * @param UriInterface $uri New uri - * @param int $statusCode Status code from the redirect response - * - * @return MessageInterface|RequestInterface + * The default only needs to be determined if no value is provided. */ - protected function buildRedirectRequest(RequestInterface $request, UriInterface $uri, $statusCode) + public function guessStreamFactory(): ?StreamFactoryInterface { - $request = $request->withUri($uri); + if (class_exists(Psr17FactoryDiscovery::class)) { + try { + return Psr17FactoryDiscovery::findStreamFactory(); + } catch (\Throwable $t) { + // ignore and try other options + } + } + if (class_exists(Psr17Factory::class)) { + return new Psr17Factory(); + } + if (class_exists(Utils::class)) { + return new class implements StreamFactoryInterface { + public function createStream(string $content = ''): StreamInterface + { + return Utils::streamFor($content); + } + + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + throw new \RuntimeException('Internal error: this method should not be needed'); + } - if (false !== $this->redirectCodes[$statusCode]['switch'] && !in_array($request->getMethod(), $this->redirectCodes[$statusCode]['switch']['unless'])) { - $request = $request->withMethod($this->redirectCodes[$statusCode]['switch']['to']); + public function createStreamFromResource($resource): StreamInterface + { + throw new \RuntimeException('Internal error: this method should not be needed'); + } + }; + } + + return null; + } + + private function buildRedirectRequest(RequestInterface $originalRequest, UriInterface $targetUri, int $statusCode): RequestInterface + { + $originalRequest = $originalRequest->withUri($targetUri); + + if (false !== $this->redirectCodes[$statusCode]['switch'] && !in_array($originalRequest->getMethod(), $this->redirectCodes[$statusCode]['switch']['unless'], true)) { + $originalRequest = $originalRequest->withMethod($this->redirectCodes[$statusCode]['switch']['to']); + if ('GET' === $this->redirectCodes[$statusCode]['switch']['to'] && $this->streamFactory) { + // if we found a stream factory, remove the request body. otherwise leave the body there. + $originalRequest = $originalRequest->withoutHeader('content-type'); + $originalRequest = $originalRequest->withoutHeader('content-length'); + $originalRequest = $originalRequest->withBody($this->streamFactory->createStream()); + } } if (is_array($this->preserveHeader)) { - $headers = array_keys($request->getHeaders()); + $headers = array_keys($originalRequest->getHeaders()); foreach ($headers as $name) { - if (!in_array($name, $this->preserveHeader)) { - $request = $request->withoutHeader($name); + if (!in_array($name, $this->preserveHeader, true)) { + $originalRequest = $originalRequest->withoutHeader($name); } } } - return $request; + return $originalRequest; } /** * Creates a new Uri from the old request and the location header. * - * @param ResponseInterface $response The redirect response - * @param RequestInterface $request The original request - * * @throws HttpException If location header is not usable (missing or incorrect) * @throws MultipleRedirectionException If a 300 status code is received and default location cannot be resolved (doesn't use the location header or not present) - * - * @return UriInterface */ - private function createUri(ResponseInterface $response, RequestInterface $request) + private function createUri(ResponseInterface $redirectResponse, RequestInterface $originalRequest): UriInterface { - if ($this->redirectCodes[$response->getStatusCode()]['multiple'] && (!$this->useDefaultForMultiple || !$response->hasHeader('Location'))) { - throw new MultipleRedirectionException('Cannot choose a redirection', $request, $response); + if ($this->redirectCodes[$redirectResponse->getStatusCode()]['multiple'] && (!$this->useDefaultForMultiple || !$redirectResponse->hasHeader('Location'))) { + throw new MultipleRedirectionException('Cannot choose a redirection', $originalRequest, $redirectResponse); } - if (!$response->hasHeader('Location')) { - throw new HttpException('Redirect status code, but no location header present in the response', $request, $response); + if (!$redirectResponse->hasHeader('Location')) { + throw new HttpException('Redirect status code, but no location header present in the response', $originalRequest, $redirectResponse); } - $location = $response->getHeaderLine('Location'); + $location = $redirectResponse->getHeaderLine('Location'); $parsedLocation = parse_url($location); - if (false === $parsedLocation) { - throw new HttpException(sprintf('Location %s could not be parsed', $location), $request, $response); + if (false === $parsedLocation || '' === $location) { + throw new HttpException(sprintf('Location "%s" could not be parsed', $location), $originalRequest, $redirectResponse); } - $uri = $request->getUri(); + $uri = $originalRequest->getUri(); + + // Redirections can either use an absolute uri or a relative reference https://www.rfc-editor.org/rfc/rfc3986#section-4.2 + // If relative, we need to check if we have an absolute path or not + + $path = array_key_exists('path', $parsedLocation) ? $parsedLocation['path'] : ''; + if (!array_key_exists('host', $parsedLocation) && '/' !== $location[0]) { + // the target is a relative-path reference, we need to merge it with the base path + $originalPath = $uri->getPath(); + if ('' === $path) { + $path = $originalPath; + } elseif (($pos = strrpos($originalPath, '/')) !== false) { + $path = substr($originalPath, 0, $pos + 1).$path; + } else { + $path = '/'.$path; + } + /* replace '/./' or '/foo/../' with '/' */ + $re = ['#(/\./)#', '#/(?!\.\.)[^/]+/\.\./#']; + for ($n = 1; $n > 0; $path = preg_replace($re, '/', $path, -1, $n)) { + if (null === $path) { + throw new HttpException(sprintf('Failed to resolve Location %s', $location), $originalRequest, $redirectResponse); + } + } + } + if (null === $path) { + throw new HttpException(sprintf('Failed to resolve Location %s', $location), $originalRequest, $redirectResponse); + } + $uri = $uri + ->withPath($path) + ->withQuery(array_key_exists('query', $parsedLocation) ? $parsedLocation['query'] : '') + ->withFragment(array_key_exists('fragment', $parsedLocation) ? $parsedLocation['fragment'] : '') + ; if (array_key_exists('scheme', $parsedLocation)) { $uri = $uri->withScheme($parsedLocation['scheme']); @@ -247,22 +332,8 @@ private function createUri(ResponseInterface $response, RequestInterface $reques if (array_key_exists('port', $parsedLocation)) { $uri = $uri->withPort($parsedLocation['port']); - } - - if (array_key_exists('path', $parsedLocation)) { - $uri = $uri->withPath($parsedLocation['path']); - } - - if (array_key_exists('query', $parsedLocation)) { - $uri = $uri->withQuery($parsedLocation['query']); - } else { - $uri = $uri->withQuery(''); - } - - if (array_key_exists('fragment', $parsedLocation)) { - $uri = $uri->withFragment($parsedLocation['fragment']); - } else { - $uri = $uri->withFragment(''); + } elseif (array_key_exists('host', $parsedLocation)) { + $uri = $uri->withPort(null); } return $uri; diff --git a/src/Plugin/RequestMatcherPlugin.php b/src/Plugin/RequestMatcherPlugin.php index 5f72b02..eb97d92 100644 --- a/src/Plugin/RequestMatcherPlugin.php +++ b/src/Plugin/RequestMatcherPlugin.php @@ -1,9 +1,12 @@ requestMatcher = $requestMatcher; - $this->delegatedPlugin = $delegatedPlugin; + $this->successPlugin = $delegateOnMatch; + $this->failurePlugin = $delegateOnNoMatch; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { if ($this->requestMatcher->matches($request)) { - return $this->delegatedPlugin->handleRequest($request, $next, $first); + if (null !== $this->successPlugin) { + return $this->successPlugin->handleRequest($request, $next, $first); + } + } elseif (null !== $this->failurePlugin) { + return $this->failurePlugin->handleRequest($request, $next, $first); } return $next($request); diff --git a/src/Plugin/RequestSeekableBodyPlugin.php b/src/Plugin/RequestSeekableBodyPlugin.php new file mode 100644 index 0000000..f39c88d --- /dev/null +++ b/src/Plugin/RequestSeekableBodyPlugin.php @@ -0,0 +1,26 @@ + + */ +final class RequestSeekableBodyPlugin extends SeekableBodyPlugin +{ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + if (!$request->getBody()->isSeekable()) { + $request = $request->withBody(new BufferedStream($request->getBody(), $this->useFileBuffer, $this->memoryBufferSize)); + } + + return $next($request); + } +} diff --git a/src/Plugin/ResponseSeekableBodyPlugin.php b/src/Plugin/ResponseSeekableBodyPlugin.php new file mode 100644 index 0000000..ef8bee4 --- /dev/null +++ b/src/Plugin/ResponseSeekableBodyPlugin.php @@ -0,0 +1,29 @@ + + */ +final class ResponseSeekableBodyPlugin extends SeekableBodyPlugin +{ + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + return $next($request)->then(function (ResponseInterface $response) { + if ($response->getBody()->isSeekable()) { + return $response; + } + + return $response->withBody(new BufferedStream($response->getBody(), $this->useFileBuffer, $this->memoryBufferSize)); + }); + } +} diff --git a/src/Plugin/RetryPlugin.php b/src/Plugin/RetryPlugin.php index 3d09265..992bb21 100644 --- a/src/Plugin/RetryPlugin.php +++ b/src/Plugin/RetryPlugin.php @@ -1,9 +1,13 @@ setDefaults([ 'retries' => 1, - 'exception_decider' => function (RequestInterface $request, Exception $e) { - return true; + 'error_response_decider' => function (RequestInterface $request, ResponseInterface $response) { + // do not retry client errors + return $response->getStatusCode() >= 500 && $response->getStatusCode() < 600; }, - 'exception_delay' => __CLASS__.'::defaultDelay', + 'exception_decider' => function (RequestInterface $request, ClientExceptionInterface $e) { + // do not retry client errors + return !$e instanceof HttpException || $e->getCode() >= 500 && $e->getCode() < 600; + }, + 'error_response_delay' => __CLASS__.'::defaultErrorResponseDelay', + 'exception_delay' => __CLASS__.'::defaultExceptionDelay', ]); + $resolver->setAllowedTypes('retries', 'int'); + $resolver->setAllowedTypes('error_response_decider', 'callable'); $resolver->setAllowedTypes('exception_decider', 'callable'); + $resolver->setAllowedTypes('error_response_delay', 'callable'); $resolver->setAllowedTypes('exception_delay', 'callable'); $options = $resolver->resolve($config); $this->retry = $options['retries']; + $this->errorResponseDecider = $options['error_response_decider']; + $this->errorResponseDelay = $options['error_response_delay']; $this->exceptionDecider = $options['exception_decider']; $this->exceptionDelay = $options['exception_delay']; } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { $chainIdentifier = spl_object_hash((object) $first); - return $next($request)->then(function (ResponseInterface $response) use ($request, $chainIdentifier) { + return $next($request)->then(function (ResponseInterface $response) use ($request, $next, $first, $chainIdentifier) { + if (!array_key_exists($chainIdentifier, $this->retryStorage)) { + $this->retryStorage[$chainIdentifier] = 0; + } + + if ($this->retryStorage[$chainIdentifier] >= $this->retry) { + unset($this->retryStorage[$chainIdentifier]); + + return $response; + } + + if (call_user_func($this->errorResponseDecider, $request, $response)) { + /** @var int $time */ + $time = call_user_func($this->errorResponseDelay, $request, $response, $this->retryStorage[$chainIdentifier]); + $response = $this->retry($request, $next, $first, $chainIdentifier, $time); + } + if (array_key_exists($chainIdentifier, $this->retryStorage)) { unset($this->retryStorage[$chainIdentifier]); } return $response; - }, function (Exception $exception) use ($request, $next, $first, $chainIdentifier) { + }, function (ClientExceptionInterface $exception) use ($request, $next, $first, $chainIdentifier) { if (!array_key_exists($chainIdentifier, $this->retryStorage)) { $this->retryStorage[$chainIdentifier] = 0; } @@ -114,26 +137,40 @@ public function handleRequest(RequestInterface $request, callable $next, callabl throw $exception; } + /** @var int $time */ $time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]); - usleep($time); - - // Retry in synchrone - ++$this->retryStorage[$chainIdentifier]; - $promise = $this->handleRequest($request, $next, $first); - return $promise->wait(); + return $this->retry($request, $next, $first, $chainIdentifier, $time); }); } /** - * @param RequestInterface $request - * @param Exception $e - * @param int $retries The number of retries we made before. First time this get called it will be 0. - * - * @return int + * @param int $retries The number of retries we made before. First time this get called it will be 0. + */ + public static function defaultErrorResponseDelay(RequestInterface $request, ResponseInterface $response, int $retries): int + { + return pow(2, $retries) * 500000; + } + + /** + * @param int $retries The number of retries we made before. First time this get called it will be 0. */ - public static function defaultDelay(RequestInterface $request, Exception $e, $retries) + public static function defaultExceptionDelay(RequestInterface $request, ClientExceptionInterface $e, int $retries): int { return pow(2, $retries) * 500000; } + + /** + * @throws \Exception if retrying returns a failed promise + */ + private function retry(RequestInterface $request, callable $next, callable $first, string $chainIdentifier, int $delay): ResponseInterface + { + usleep($delay); + + // Retry synchronously + ++$this->retryStorage[$chainIdentifier]; + $promise = $this->handleRequest($request, $next, $first); + + return $promise->wait(); + } } diff --git a/src/Plugin/SeekableBodyPlugin.php b/src/Plugin/SeekableBodyPlugin.php new file mode 100644 index 0000000..1be2cde --- /dev/null +++ b/src/Plugin/SeekableBodyPlugin.php @@ -0,0 +1,47 @@ +setDefaults([ + 'use_file_buffer' => true, + 'memory_buffer_size' => 2097152, + ]); + $resolver->setAllowedTypes('use_file_buffer', 'bool'); + $resolver->setAllowedTypes('memory_buffer_size', 'int'); + + $options = $resolver->resolve($config); + + $this->useFileBuffer = $options['use_file_buffer']; + $this->memoryBufferSize = $options['memory_buffer_size']; + } +} diff --git a/src/Plugin/VersionBridgePlugin.php b/src/Plugin/VersionBridgePlugin.php index f3891e5..0a2c714 100644 --- a/src/Plugin/VersionBridgePlugin.php +++ b/src/Plugin/VersionBridgePlugin.php @@ -1,7 +1,10 @@ doHandleRequest($request, $next, $first); } diff --git a/src/PluginChain.php b/src/PluginChain.php new file mode 100644 index 0000000..2bfbfbc --- /dev/null +++ b/src/PluginChain.php @@ -0,0 +1,61 @@ +plugins = $plugins; + $this->clientCallable = $clientCallable; + $this->maxRestarts = (int) ($options['max_restarts'] ?? 0); + } + + private function createChain(): callable + { + $lastCallable = $this->clientCallable; + $reversedPlugins = \array_reverse($this->plugins); + + foreach ($reversedPlugins as $plugin) { + $lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable) { + return $plugin->handleRequest($request, $lastCallable, $this); + }; + } + + return $lastCallable; + } + + public function __invoke(RequestInterface $request): Promise + { + if ($this->restarts > $this->maxRestarts) { + throw new LoopException('Too many restarts in plugin client', $request); + } + + ++$this->restarts; + + return $this->createChain()($request); + } +} diff --git a/src/PluginClient.php b/src/PluginClient.php index 625033b..d728c78 100644 --- a/src/PluginClient.php +++ b/src/PluginClient.php @@ -1,15 +1,18 @@ client = $client; - } elseif ($client instanceof HttpClient || $client instanceof ClientInterface) { + } elseif ($client instanceof ClientInterface) { $this->client = new EmulatedHttpAsyncClient($client); } else { - throw new \RuntimeException('Client must be an instance of Http\\Client\\HttpClient or Http\\Client\\HttpAsyncClient'); + throw new \TypeError( + sprintf('%s::__construct(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); } $this->plugins = $plugins; $this->options = $this->configure($options); } - /** - * {@inheritdoc} - */ - public function sendRequest(RequestInterface $request) + public function sendRequest(RequestInterface $request): ResponseInterface { - // If we don't have an http client, use the async call - if (!($this->client instanceof HttpClient)) { + // If the client doesn't support sync calls, call async + if (!$this->client instanceof ClientInterface) { return $this->sendAsyncRequest($request)->wait(); } - // Else we want to use the synchronous call of the underlying client, and not the async one in the case - // we have both an async and sync call + // Else we want to use the synchronous call of the underlying client, + // and not the async one in the case we have both an async and sync call $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) { try { return new HttpFulfilledPromise($this->client->sendRequest($request)); @@ -88,9 +84,6 @@ public function sendRequest(RequestInterface $request) return $pluginChain($request)->wait(); } - /** - * {@inheritdoc} - */ public function sendAsyncRequest(RequestInterface $request) { $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) { @@ -102,35 +95,15 @@ public function sendAsyncRequest(RequestInterface $request) /** * Configure the plugin client. - * - * @param array $options - * - * @return array */ - private function configure(array $options = []) + private function configure(array $options = []): array { - if (isset($options['debug_plugins'])) { - @trigger_error('The "debug_plugins" option is deprecated since 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); - } - $resolver = new OptionsResolver(); $resolver->setDefaults([ 'max_restarts' => 10, - 'debug_plugins' => [], ]); - $resolver - ->setAllowedTypes('debug_plugins', 'array') - ->setAllowedValues('debug_plugins', function (array $plugins) { - foreach ($plugins as $plugin) { - // Make sure each object passed with the `debug_plugins` is an instance of Plugin. - if (!$plugin instanceof Plugin) { - return false; - } - } - - return true; - }); + $resolver->setAllowedTypes('max_restarts', 'int'); return $resolver->resolve($options); } @@ -138,43 +111,13 @@ private function configure(array $options = []) /** * Create the plugin chain. * - * @param Plugin[] $pluginList A list of plugins + * @param Plugin[] $plugins A plugin chain * @param callable $clientCallable Callable making the HTTP call * - * @return callable + * @return callable(RequestInterface): Promise */ - private function createPluginChain($pluginList, callable $clientCallable) + private function createPluginChain(array $plugins, callable $clientCallable): callable { - $firstCallable = $lastCallable = $clientCallable; - - /* - * Inject debug plugins between each plugin. - */ - $pluginListWithDebug = $this->options['debug_plugins']; - foreach ($pluginList as $plugin) { - $pluginListWithDebug[] = $plugin; - $pluginListWithDebug = array_merge($pluginListWithDebug, $this->options['debug_plugins']); - } - - while ($plugin = array_pop($pluginListWithDebug)) { - $lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable, &$firstCallable) { - return $plugin->handleRequest($request, $lastCallable, $firstCallable); - }; - - $firstCallable = $lastCallable; - } - - $firstCalls = 0; - $firstCallable = function (RequestInterface $request) use ($lastCallable, &$firstCalls) { - if ($firstCalls > $this->options['max_restarts']) { - throw new LoopException('Too many restarts in plugin client', $request); - } - - ++$firstCalls; - - return $lastCallable($request); - }; - - return $firstCallable; + return new PluginChain($plugins, $clientCallable, $this->options); } } diff --git a/src/PluginClientBuilder.php b/src/PluginClientBuilder.php new file mode 100644 index 0000000..45fd787 --- /dev/null +++ b/src/PluginClientBuilder.php @@ -0,0 +1,76 @@ + + */ +final class PluginClientBuilder +{ + /** @var Plugin[][] List of plugins ordered by priority [priority => Plugin[]]). */ + private $plugins = []; + + /** @var array Array of options to give to the plugin client */ + private $options = []; + + /** + * @param int $priority Priority of the plugin. The higher comes first. + */ + public function addPlugin(Plugin $plugin, int $priority = 0): self + { + $this->plugins[$priority][] = $plugin; + + return $this; + } + + /** + * @param string|int|float|bool|string[] $value + */ + public function setOption(string $name, $value): self + { + $this->options[$name] = $value; + + return $this; + } + + public function removeOption(string $name): self + { + unset($this->options[$name]); + + return $this; + } + + /** + * @param ClientInterface|HttpAsyncClient $client + */ + public function createClient($client): PluginClient + { + if (!$client instanceof ClientInterface && !$client instanceof HttpAsyncClient) { + throw new \TypeError( + sprintf('%s::createClient(): Argument #1 ($client) must be of type %s|%s, %s given', self::class, ClientInterface::class, HttpAsyncClient::class, get_debug_type($client)) + ); + } + + $plugins = $this->plugins; + + if (0 === count($plugins)) { + $plugins[] = []; + } + + krsort($plugins); + $plugins = array_merge(...$plugins); + + return new PluginClient( + $client, + array_values($plugins), + $this->options + ); + } +} diff --git a/src/PluginClientFactory.php b/src/PluginClientFactory.php index cbea3e1..1d2b2be 100644 --- a/src/PluginClientFactory.php +++ b/src/PluginClientFactory.php @@ -1,9 +1,10 @@ doSendRequest($request); } diff --git a/tests/HttpMethodsClientTest.php b/tests/HttpMethodsClientTest.php new file mode 100644 index 0000000..d93a2a8 --- /dev/null +++ b/tests/HttpMethodsClientTest.php @@ -0,0 +1,100 @@ +httpClient = $this->createMock(ClientInterface::class); + $streamFactory = $requestFactory = new Psr17Factory(); + $this->httpMethodsClient = new HttpMethodsClient($this->httpClient, $requestFactory, $streamFactory); + } + + public function testGet(): void + { + $this->expectSendRequest('get'); + } + + public function testHead(): void + { + $this->expectSendRequest('head'); + } + + public function testTrace(): void + { + $this->expectSendRequest('trace'); + } + + public function testPost(): void + { + $this->expectSendRequest('post', self::BODY); + } + + public function testPut(): void + { + $this->expectSendRequest('put', self::BODY); + } + + public function testPatch(): void + { + $this->expectSendRequest('patch', self::BODY); + } + + public function testDelete(): void + { + $this->expectSendRequest('delete', self::BODY); + } + + public function testOptions(): void + { + $this->expectSendRequest('options', self::BODY); + } + + /** + * Run the actual test. + * + * As there is no data provider in phpspec, we keep separate methods to get new mocks for each test. + */ + private function expectSendRequest(string $method, ?string $body = null): void + { + $response = new Response(); + $this->httpClient->expects($this->once()) + ->method('sendRequest') + ->with(self::callback(static function (RequestInterface $request) use ($body, $method): bool { + self::assertSame(strtoupper($method), $request->getMethod()); + self::assertSame(self::URI, (string) $request->getUri()); + self::assertSame([self::HEADER_NAME => [self::HEADER_VALUE]], $request->getHeaders()); + self::assertSame((string) $body, (string) $request->getBody()); + + return true; + })) + ->willReturn($response) + ; + + $actualResponse = $this->httpMethodsClient->$method(self::URI, [self::HEADER_NAME => self::HEADER_VALUE], self::BODY); + $this->assertSame($response, $actualResponse); + } +} diff --git a/tests/Plugin/AddPathPluginTest.php b/tests/Plugin/AddPathPluginTest.php new file mode 100644 index 0000000..9457535 --- /dev/null +++ b/tests/Plugin/AddPathPluginTest.php @@ -0,0 +1,96 @@ +first = function () {}; + $this->plugin = new AddPathPlugin(new Uri('/api')); + } + + public function testRewriteSameUrl() + { + $verify = function (RequestInterface $request) { + $this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString()); + + return new HttpFulfilledPromise(new Response()); + }; + + $request = new Request('GET', 'https://example.com/foo', ['Content-Type' => 'text/html']); + $this->plugin->handleRequest($request, $verify, $this->first); + + // Make a second call with the same $request object + $this->plugin->handleRequest($request, $verify, $this->first); + + // Make a new call with a new object but same URL + $request = new Request('GET', 'https://example.com/foo', ['Content-Type' => 'text/plain']); + $this->plugin->handleRequest($request, $verify, $this->first); + } + + public function testRewriteCallingThePluginTwice() + { + $request = new Request('GET', 'https://example.com/foo'); + $this->plugin->handleRequest($request, function (RequestInterface $request) { + $this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString()); + + // Run the plugin again with the modified request + $this->plugin->handleRequest($request, function (RequestInterface $request) { + $this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString()); + + return new HttpFulfilledPromise(new Response()); + }, $this->first); + + return new HttpFulfilledPromise(new Response()); + }, $this->first); + } + + public function testRewriteWithDifferentUrl() + { + $request = new Request('GET', 'https://example.com/foo'); + $this->plugin->handleRequest($request, function (RequestInterface $request) { + $this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString()); + + return new HttpFulfilledPromise(new Response()); + }, $this->first); + + $request = new Request('GET', 'https://example.com/bar'); + $this->plugin->handleRequest($request, function (RequestInterface $request) { + $this->assertEquals('https://example.com/api/bar', $request->getUri()->__toString()); + + return new HttpFulfilledPromise(new Response()); + }, $this->first); + } + + public function testRewriteWhenPathIsIncluded() + { + $verify = function (RequestInterface $request) { + $this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString()); + + return new HttpFulfilledPromise(new Response()); + }; + + $request = new Request('GET', 'https://example.com/api/foo'); + $this->plugin->handleRequest($request, $verify, $this->first); + } +} diff --git a/tests/Plugin/RedirectPluginTest.php b/tests/Plugin/RedirectPluginTest.php new file mode 100644 index 0000000..92072ad --- /dev/null +++ b/tests/Plugin/RedirectPluginTest.php @@ -0,0 +1,116 @@ +expectException(CircularRedirectionException::class); + (new RedirectPlugin())->handleRequest( + new Request('GET', 'https://example.com/path?query=value'), + function () { + return new FulfilledPromise(new Response(302, ['Location' => 'https://example.com/path?query=value'])); + }, + function () {} + )->wait(); + } + + public function testPostGetDropRequestBody(): void + { + $response = (new RedirectPlugin())->handleRequest( + new Request('POST', 'https://example.com/path', ['Content-Type' => 'text/plain', 'Content-Length' => '10'], (new Psr17Factory())->createStream('hello test')), + function (RequestInterface $request) { + $this->assertSame(10, $request->getBody()->getSize()); + $this->assertTrue($request->hasHeader('Content-Type')); + $this->assertTrue($request->hasHeader('Content-Length')); + + return new FulfilledPromise(new Response(302, ['Location' => 'https://example.com/other'])); + }, + function (RequestInterface $request) { + $this->assertSame('GET', $request->getMethod()); + $this->assertSame(0, $request->getBody()->getSize()); + $this->assertFalse($request->hasHeader('Content-Type')); + $this->assertFalse($request->hasHeader('Content-Length')); + + return new FulfilledPromise(new Response(200, ['uri' => $request->getUri()->__toString()])); + } + )->wait(); + + $this->assertSame('https://example.com/other', $response->getHeaderLine('uri')); + } + + public function testPostGetNoFactory(): void + { + // We explicitly set the stream factory to null. Same happens if no factory can be found. + // In this case, the redirect will leave the body alone. + $response = (new RedirectPlugin(['stream_factory' => null]))->handleRequest( + new Request('POST', 'https://example.com/path', ['Content-Type' => 'text/plain', 'Content-Length' => '10'], (new Psr17Factory())->createStream('hello test')), + function (RequestInterface $request) { + $this->assertSame(10, $request->getBody()->getSize()); + $this->assertTrue($request->hasHeader('Content-Type')); + $this->assertTrue($request->hasHeader('Content-Length')); + + return new FulfilledPromise(new Response(302, ['Location' => 'https://example.com/other'])); + }, + function (RequestInterface $request) { + $this->assertSame('GET', $request->getMethod()); + $this->assertSame(10, $request->getBody()->getSize()); + $this->assertTrue($request->hasHeader('Content-Type')); + $this->assertTrue($request->hasHeader('Content-Length')); + + return new FulfilledPromise(new Response(200, ['uri' => $request->getUri()->__toString()])); + } + )->wait(); + + $this->assertSame('https://example.com/other', $response->getHeaderLine('uri')); + } + + public function provideRedirections(): array + { + return [ + 'no path on target' => ['https://example.com/path?query=value', 'https://example.com?query=value', 'https://example.com?query=value'], + 'root path on target' => ['https://example.com/path?query=value', 'https://example.com/?query=value', 'https://example.com/?query=value'], + 'redirect to query' => ['https://example.com', 'https://example.com?query=value', 'https://example.com?query=value'], + 'redirect to different domain without port' => ['https://example.com:8000', 'https://foo.com?query=value', 'https://foo.com?query=value'], + 'network-path redirect, preserve scheme' => ['https://example.com:8000', '//foo.com/path?query=value', 'https://foo.com/path?query=value'], + 'absolute-path redirect, preserve host' => ['https://example.com:8000', '/path?query=value', 'https://example.com:8000/path?query=value'], + 'relative-path redirect, append' => ['https://example.com:8000/path/', 'sub/path?query=value', 'https://example.com:8000/path/sub/path?query=value'], + 'relative-path on non-folder' => ['https://example.com:8000/path/foo', 'sub/path?query=value', 'https://example.com:8000/path/sub/path?query=value'], + 'relative-path moving up' => ['https://example.com:8000/path/', '../other?query=value', 'https://example.com:8000/other?query=value'], + 'relative-path with ./' => ['https://example.com:8000/path/', './other?query=value', 'https://example.com:8000/path/other?query=value'], + 'relative-path with //' => ['https://example.com:8000/path/', 'other//sub?query=value', 'https://example.com:8000/path/other//sub?query=value'], + 'relative-path redirect with only query' => ['https://example.com:8000/path', '?query=value', 'https://example.com:8000/path?query=value'], + ]; + } + + /** + * @dataProvider provideRedirections + */ + public function testTargetUriMappingFromLocationHeader(string $originalUri, string $locationUri, string $targetUri): void + { + $response = (new RedirectPlugin())->handleRequest( + new Request('GET', $originalUri), + function () use ($locationUri) { + return new FulfilledPromise(new Response(302, ['Location' => $locationUri])); + }, + function (RequestInterface $request) { + return new FulfilledPromise(new Response(200, ['uri' => $request->getUri()->__toString()])); + } + )->wait(); + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertSame($targetUri, $response->getHeaderLine('uri')); + } +} diff --git a/tests/PluginChainTest.php b/tests/PluginChainTest.php new file mode 100644 index 0000000..7a53681 --- /dev/null +++ b/tests/PluginChainTest.php @@ -0,0 +1,91 @@ +func = $func; + } + + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + ($this->func)($request, $next, $first); + + return $next($request); + } + }; + } + + public function testChainShouldInvokePluginsInReversedOrder(): void + { + $pluginOrderCalls = []; + + $plugin1 = $this->createPlugin(static function () use (&$pluginOrderCalls) { + $pluginOrderCalls[] = 'plugin1'; + }); + $plugin2 = $this->createPlugin(static function () use (&$pluginOrderCalls) { + $pluginOrderCalls[] = 'plugin2'; + }); + + $request = $this->prophesize(RequestInterface::class); + $responsePromise = $this->prophesize(Promise::class); + + $clientCallable = static function () use ($responsePromise) { + return $responsePromise->reveal(); + }; + + $pluginOrderCalls = []; + + $plugins = [ + $plugin1, + $plugin2, + ]; + + $pluginChain = new PluginChain($plugins, $clientCallable); + + $result = $pluginChain($request->reveal()); + + $this->assertSame($responsePromise->reveal(), $result); + $this->assertSame(['plugin1', 'plugin2'], $pluginOrderCalls); + } + + public function testShouldThrowLoopExceptionOnMaxRestarts(): void + { + $this->expectException(LoopException::class); + + $request = $this->prophesize(RequestInterface::class); + $responsePromise = $this->prophesize(Promise::class); + $calls = 0; + $clientCallable = static function () use ($responsePromise, &$calls) { + ++$calls; + + return $responsePromise->reveal(); + }; + + $pluginChain = new PluginChain([], $clientCallable, ['max_restarts' => 2]); + + $pluginChain($request->reveal()); + $this->assertSame(1, $calls); + $pluginChain($request->reveal()); + $this->assertSame(2, $calls); + $pluginChain($request->reveal()); + $this->assertSame(3, $calls); + $pluginChain($request->reveal()); + } +} diff --git a/tests/PluginClientBuilderTest.php b/tests/PluginClientBuilderTest.php new file mode 100644 index 0000000..9372859 --- /dev/null +++ b/tests/PluginClientBuilderTest.php @@ -0,0 +1,79 @@ + $this->prophesize(Plugin::class)->reveal(), + -10 => $this->prophesize(Plugin::class)->reveal(), + 0 => $this->prophesize(Plugin::class)->reveal(), + ]; + + foreach ($plugins as $priority => $plugin) { + $builder->addPlugin($plugin, $priority); + } + + $client = $this->prophesize($client)->reveal(); + $client = $builder->createClient($client); + + $closure = \Closure::bind( + function (): array { + return $this->plugins; + }, + $client, + PluginClient::class + ); + + $plugged = $closure(); + + $expected = $plugins; + krsort($expected); + $expected = array_values($expected); + + $this->assertSame($expected, $plugged); + } + + /** @dataProvider clientProvider */ + public function testOptions(string $client): void + { + $builder = new PluginClientBuilder(); + $builder->setOption('max_restarts', 5); + + $client = $this->prophesize($client)->reveal(); + $client = $builder->createClient($client); + + $closure = \Closure::bind( + function (): array { + return $this->options; + }, + $client, + PluginClient::class + ); + + $options = $closure(); + + $this->assertArrayHasKey('max_restarts', $options); + $this->assertSame(5, $options['max_restarts']); + } + + public function clientProvider(): iterable + { + yield 'sync\'d http client' => [HttpClient::class]; + yield 'async\'d http client' => [HttpAsyncClient::class]; + } +} diff --git a/tests/PluginClientTest.php b/tests/PluginClientTest.php new file mode 100644 index 0000000..26547bd --- /dev/null +++ b/tests/PluginClientTest.php @@ -0,0 +1,79 @@ +assertInstanceOf($returnType, $result); + } + + public function clientAndMethodProvider() + { + $syncClient = new class implements ClientInterface { + public function sendRequest(RequestInterface $request): ResponseInterface + { + return new Response(); + } + }; + + $asyncClient = new class implements HttpAsyncClient { + public function sendAsyncRequest(RequestInterface $request) + { + return new HttpFulfilledPromise(new Response()); + } + }; + + $headerAppendPlugin = new HeaderAppendPlugin(['Content-Type' => 'text/html']); + $redirectPlugin = new RedirectPlugin(); + $restartOncePlugin = new class implements Plugin { + private $firstRun = true; + + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + if ($this->firstRun) { + $this->firstRun = false; + + return $first($request); + } + $this->firstRun = true; + + return $next($request); + } + }; + + $plugins = [$headerAppendPlugin, $restartOncePlugin, $redirectPlugin]; + + $pluginClient = new PluginClient($syncClient, $plugins); + yield [$pluginClient, 'sendRequest', ResponseInterface::class]; + yield [$pluginClient, 'sendAsyncRequest', Promise::class]; + + // Async + $pluginClient = new PluginClient($asyncClient, $plugins); + yield [$pluginClient, 'sendRequest', ResponseInterface::class]; + yield [$pluginClient, 'sendAsyncRequest', Promise::class]; + } +}