From 7f2221488502e5208a5047991ee4e6cd4966bbdf Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 22 Jul 2026 10:51:11 +0200 Subject: [PATCH 1/5] phpstan: Drop reportUnmatchedIgnoredErrors We do not ignore any errors. And it is better to specify it for individual errors with custom explanation for each. If we introduce baseline, it should only contain universal errors. https://phpstan.org/user-guide/baseline --- phpstan.neon.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d42940e..91e6453 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,4 @@ parameters: level: 5 - reportUnmatchedIgnoredErrors: false paths: - src From 54ba41b09c54fd1f0ff570336db89114239d1cd7 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 22 Jul 2026 11:16:44 +0200 Subject: [PATCH 2/5] tests/PromiseException: Use generator for provider It will allow us to make some exceptions conditional. --- tests/PromiseExceptionTest.php | 50 +++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/tests/PromiseExceptionTest.php b/tests/PromiseExceptionTest.php index a80083e..d4633ba 100644 --- a/tests/PromiseExceptionTest.php +++ b/tests/PromiseExceptionTest.php @@ -38,28 +38,40 @@ public function testExceptionThatIsThrownForGuzzleException( $promise->wait(); } - public static function exceptionThatIsThrownForGuzzleExceptionProvider(): array + public static function exceptionThatIsThrownForGuzzleExceptionProvider(): iterable { $request = (new PromiseExceptionTest('request'))->getMockBuilder(RequestInterface::class)->getMock(); $response = (new PromiseExceptionTest('response'))->getMockBuilder(ResponseInterface::class)->getMock(); - return [ - [$request, new GuzzleExceptions\ConnectException('foo', $request), NetworkException::class], - [$request, new GuzzleExceptions\TooManyRedirectsException('foo', $request), RequestException::class], - [$request, new GuzzleExceptions\RequestException('foo', $request, $response), HttpException::class], - [$request, new GuzzleExceptions\BadResponseException('foo', $request, $response), HttpException::class], - [$request, new GuzzleExceptions\ClientException('foo', $request, $response), HttpException::class], - [$request, new GuzzleExceptions\ServerException('foo', $request, $response), HttpException::class], - [$request, new GuzzleExceptions\TransferException('foo'), TransferException::class], - // check cases without response - [$request, new GuzzleExceptions\RequestException('foo', $request), RequestException::class], - [$request, new GuzzleExceptions\BadResponseException('foo', $request, $response), RequestException::class], - [$request, new GuzzleExceptions\ClientException('foo', $request, $response), RequestException::class], - [$request, new GuzzleExceptions\ServerException('foo', $request, $response), RequestException::class], - // Non PSR-18 Exceptions thrown - [$request, new \Exception('foo'), TransferException::class], - [$request, new \Error('foo'), TransferException::class], - [$request, 'whatever', UnexpectedValueException::class], - ]; + + yield [$request, new GuzzleExceptions\ConnectException('foo', $request), NetworkException::class]; + + yield [$request, new GuzzleExceptions\TooManyRedirectsException('foo', $request), RequestException::class]; + + yield [$request, new GuzzleExceptions\RequestException('foo', $request, $response), HttpException::class]; + + yield [$request, new GuzzleExceptions\BadResponseException('foo', $request, $response), HttpException::class]; + + yield [$request, new GuzzleExceptions\ClientException('foo', $request, $response), HttpException::class]; + + yield [$request, new GuzzleExceptions\ServerException('foo', $request, $response), HttpException::class]; + + yield [$request, new GuzzleExceptions\TransferException('foo'), TransferException::class]; + + // check cases without response + yield [$request, new GuzzleExceptions\RequestException('foo', $request), RequestException::class]; + + yield [$request, new GuzzleExceptions\BadResponseException('foo', $request, $response), RequestException::class]; + + yield [$request, new GuzzleExceptions\ClientException('foo', $request, $response), RequestException::class]; + + yield [$request, new GuzzleExceptions\ServerException('foo', $request, $response), RequestException::class]; + + // Non PSR-18 Exceptions thrown + yield [$request, new \Exception('foo'), TransferException::class]; + + yield [$request, new \Error('foo'), TransferException::class]; + + yield [$request, 'whatever', UnexpectedValueException::class]; } } From b78bbf50b768c0748f59cb425c404ee0c23780b6 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 22 Jul 2026 10:56:49 +0200 Subject: [PATCH 3/5] tests: Add missing response to exception With Guzzle 8, `TooManyRedirectsException` extends `ResponseException`, which expects a third `$response` parameter, making PHPStan fail with: Class GuzzleHttp\Exception\TooManyRedirectsException constructor invoked with 2 parameters, 3-4 required. This also matches what Guzzle 7 itself does: https://github.com/guzzle/guzzle/blob/7.10.0/src/RedirectMiddleware.php#L155 --- tests/PromiseExceptionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PromiseExceptionTest.php b/tests/PromiseExceptionTest.php index d4633ba..b64db5e 100644 --- a/tests/PromiseExceptionTest.php +++ b/tests/PromiseExceptionTest.php @@ -46,7 +46,7 @@ public static function exceptionThatIsThrownForGuzzleExceptionProvider(): iterab yield [$request, new GuzzleExceptions\ConnectException('foo', $request), NetworkException::class]; - yield [$request, new GuzzleExceptions\TooManyRedirectsException('foo', $request), RequestException::class]; + yield [$request, new GuzzleExceptions\TooManyRedirectsException('foo', $request, $response), RequestException::class]; yield [$request, new GuzzleExceptions\RequestException('foo', $request, $response), HttpException::class]; From 7a5d7e8692c6f5456207d53ad87b70ec10f5e050 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 22 Jul 2026 10:57:07 +0200 Subject: [PATCH 4/5] phpstan: Check tests This will make it easier to notice discrepancies. Had to add some ignores to suppress errors from differences between versions. --- phpstan.neon.dist | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 91e6453..84202ba 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,3 +2,29 @@ parameters: level: 5 paths: - src + - tests + + ignoreErrors: + - + message: '#^Non\-static access to static property Http\\Client\\Tests\\HttpBaseTest\:\:\$defaultHeaders\.$#' + identifier: staticProperty.nonStaticAccess + count: 1 + # Only occurs with php-http/client-integration-tests ≥ 4.0.0 + reportUnmatched: false + path: tests/DefaultHttpAdapterWithConfigTest.php + + - + message: '#^Static access to instance property Http\\Client\\Tests\\HttpBaseTest\:\:\$defaultHeaders\.$#' + identifier: property.staticAccess + count: 1 + # Only occurs with php-http/client-integration-tests < 4.0.0 + reportUnmatched: false + path: tests/DefaultHttpAdapterWithConfigTest.php + + - + message: '#^Attribute class PHPUnit\\Framework\\Attributes\\DataProvider does not exist\.$#' + identifier: attribute.notFound + count: 1 + # Only occurs with phpunit/phpunit < 10.0.0 + reportUnmatched: false + path: tests/PromiseExceptionTest.php From 8a252257a33b5fdf015fd7609037266c7a440861 Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 22 Jul 2026 10:49:39 +0200 Subject: [PATCH 5/5] Add support for Guzzle 8 This is a bit stricter but mostly compatible so not much reason to create a new project. Changes affecting us involve improved type annotations and splitting out `ResponseException`. https://github.com/guzzle/guzzle/releases/8.0.0 --- CHANGELOG.md | 6 +++++ README.md | 4 ++-- composer.json | 2 +- phpstan.neon.dist | 40 ++++++++++++++++++++++++++++++++++ src/Promise.php | 3 ++- tests/PromiseExceptionTest.php | 15 +++++++++++-- 6 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c2e2c..c8a4800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.2.0] - unreleased + +### Added + +- Add support for Guzzle 8 + ## [1.1.0] - 2024-11-26 ### Changed diff --git a/README.md b/README.md index 7e6b5b5..aab7c76 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Guzzle 7 HTTP Adapter +# Guzzle 7/8 HTTP Adapter [![Latest Version](https://img.shields.io/github/release/php-http/guzzle7-adapter.svg?style=flat-square)](https://github.com/php-http/guzzle7-adapter/releases) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) [![Total Downloads](https://img.shields.io/packagist/dt/php-http/guzzle7-adapter.svg?style=flat-square)](https://packagist.org/packages/php-http/guzzle7-adapter) -**Guzzle 7 HTTP Adapter.** +**Guzzle 7/8 HTTP Adapter.** ## Install diff --git a/composer.json b/composer.json index 55ce418..09abcef 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "php": "^7.3 | ^8.0", "php-http/httplug": "^2.4", "psr/http-client": "^1.0.3", - "guzzlehttp/guzzle": "^7.10" + "guzzlehttp/guzzle": "^7.10 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.6.31 || ^10.0 || ^11.0 || ^12.0", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 84202ba..83ce653 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -28,3 +28,43 @@ parameters: # Only occurs with phpunit/phpunit < 10.0.0 reportUnmatched: false path: tests/PromiseExceptionTest.php + + - + message: '#^Call to function method_exists\(\) with GuzzleHttp\\Exception\\RequestException and ''getResponse'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + # Only occurs with guzzlehttp/guzzle < 8.0.0 + reportUnmatched: false + path: src/Promise.php + + - + message: '#^Call to function method_exists\(\) with GuzzleHttp\\Exception\\RequestException and ''hasResponse'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + # Only occurs with guzzlehttp/guzzle < 8.0.0 + reportUnmatched: false + path: src/Promise.php + + - + message: '#^Parameter \#2 \$code of class GuzzleHttp\\Exception\\TransferException constructor expects int, PHPUnit\\Framework\\MockObject\\MockObject&Psr\\Http\\Message\\RequestInterface given\.$#' + identifier: argument.type + count: 1 + # Only occurs with guzzlehttp/guzzle < 8.0.0 + reportUnmatched: false + path: tests/PromiseExceptionTest.php + + - + message: '#^Call to function method_exists\(\) with ''GuzzleHttp\\\\Exception\\\\TransferException'' and ''getRequest'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + # Only occurs with guzzlehttp/guzzle ≥ 8.0.0 + reportUnmatched: false + path: tests/PromiseExceptionTest.php + + - + message: '#^Class GuzzleHttp\\Exception\\TransferException constructor invoked with 1 parameter, 2\-4 required\.$#' + identifier: arguments.count + count: 1 + # Only occurs with guzzlehttp/guzzle ≥ 8.0.0 + reportUnmatched: false + path: tests/PromiseExceptionTest.php diff --git a/src/Promise.php b/src/Promise.php index 14a8812..4b92c8a 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -109,7 +109,8 @@ private function handleException(GuzzleExceptions\GuzzleException $exception) if ($exception instanceof GuzzleExceptions\RequestException) { // Make sure we have a response for the HttpException - if ($exception->hasResponse()) { + if ((class_exists(GuzzleExceptions\ResponseException::class) && $exception instanceof GuzzleExceptions\ResponseException) + || (method_exists($exception, 'hasResponse') && method_exists($exception, 'getResponse') && $exception->hasResponse())) { return new HttplugException\HttpException( $exception->getMessage(), $exception->getRequest(), diff --git a/tests/PromiseExceptionTest.php b/tests/PromiseExceptionTest.php index b64db5e..18c69ef 100644 --- a/tests/PromiseExceptionTest.php +++ b/tests/PromiseExceptionTest.php @@ -48,7 +48,12 @@ public static function exceptionThatIsThrownForGuzzleExceptionProvider(): iterab yield [$request, new GuzzleExceptions\TooManyRedirectsException('foo', $request, $response), RequestException::class]; - yield [$request, new GuzzleExceptions\RequestException('foo', $request, $response), HttpException::class]; + yield [$request, new GuzzleExceptions\RequestException('foo', $request), HttpException::class]; + + if (class_exists(GuzzleExceptions\ResponseException::class)) { + // Guzzle 8 + yield [$request, new GuzzleExceptions\ResponseException('foo', $request, $response), HttpException::class]; + } yield [$request, new GuzzleExceptions\BadResponseException('foo', $request, $response), HttpException::class]; @@ -56,7 +61,13 @@ public static function exceptionThatIsThrownForGuzzleExceptionProvider(): iterab yield [$request, new GuzzleExceptions\ServerException('foo', $request, $response), HttpException::class]; - yield [$request, new GuzzleExceptions\TransferException('foo'), TransferException::class]; + if (method_exists(GuzzleExceptions\TransferException::class, 'getRequest')) { + // Guzzle 8 + yield [$request, new GuzzleExceptions\TransferException('foo', $request), TransferException::class]; + } else { + // Guzzle 7 + yield [$request, new GuzzleExceptions\TransferException('foo'), TransferException::class]; + } // check cases without response yield [$request, new GuzzleExceptions\RequestException('foo', $request), RequestException::class];